"use client"; import { closestCenter, DndContext, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors, type DragEndEvent, } from "@dnd-kit/core"; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { BotMessageSquareIcon, GripVerticalIcon, MoreHorizontalIcon, PlusIcon, PowerIcon, RefreshCwIcon, SearchIcon, Trash2Icon, } from "lucide-react"; import { useCallback, useEffect, useState, type CSSProperties, } from "react"; import { toast } from "sonner"; import { DashboardPage, DashboardTableShell, DashboardTableStateRow, DashboardToolbar, } from "@/components/dashboard-page"; import { ListPagination } from "@/components/list-pagination"; import { OptionCombobox } from "@/components/option-combobox"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { createAIAgent, deleteAIAgent, fetchAIAgents, updateAIAgent, updateAIAgentSort, updateAIAgentStatus, type AIAgent, type CreateAIAgentPayload, type PageResult, } from "@/lib/api/admin"; import { IMConversationServiceMode, Status } from "@/lib/generated/enums"; import { useI18n } from "@/i18n/provider"; import { cn } from "@/lib/utils"; import { EditDialog } from "./_components/edit"; import { ButtonGroup } from "@/components/ui/button-group"; type TFunction = (key: string, values?: Record) => string; function getStatusOptions(t: TFunction) { return [ { value: "all", label: t("aiAgent.allStatuses") }, { value: String(Status.Ok), label: t("aiAgent.enabled") }, { value: String(Status.Disabled), label: t("aiAgent.disabled") }, { value: String(Status.Deleted), label: t("status.deleted") }, ]; } function getStatusLabel(value: string, t: TFunction) { return ( getStatusOptions(t).find((item) => item.value === value)?.label ?? t("aiAgent.allStatuses") ); } function getServiceModeLabel(mode: number, t: TFunction) { switch (mode) { case IMConversationServiceMode.AIOnly: return t("aiAgent.serviceAiOnly"); case IMConversationServiceMode.HumanOnly: return t("aiAgent.serviceHumanOnly"); case IMConversationServiceMode.AIFirst: return t("aiAgent.serviceAiFirst"); default: return "-"; } } type SortableAIAgentRowProps = { item: AIAgent; disabled: boolean; actionLoadingId: number | null; t: TFunction; openEditDialog: (item: AIAgent) => void; handleToggleStatus: (item: AIAgent) => void; handleDelete: (item: AIAgent) => void; }; function SortableAIAgentRow({ item, disabled, actionLoadingId, t, openEditDialog, handleToggleStatus, handleDelete, }: SortableAIAgentRowProps) { const knowledgeIds = item.knowledgeIds ?? []; const knowledgeBaseNames = item.knowledgeBaseNames ?? []; const skills = item.skills ?? []; const directTools = item.directTools ?? []; const directToolServerCodes = Array.from( new Set(directTools.map((tool) => tool.serverCode).filter(Boolean)), ); const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: item.id, disabled, }); const style: CSSProperties = { transform: CSS.Transform.toString(transform), transition, }; return (
{item.name}
{item.aiConfigName || "-"} {getServiceModeLabel(item.serviceMode, t)}
{knowledgeIds.length === 0 ? ( {t("aiAgent.notConfigured")} ) : ( knowledgeBaseNames.map((name, index) => ( {name} )) )}
{skills.length === 0 ? ( {t("aiAgent.ragOnly")} ) : ( skills.map((skill) => ( {skill.name} )) )}
{skills.length} Skills {directTools.length} Tools
{directToolServerCodes.length === 0 ? ( {t("aiAgent.noMcpServer")} ) : ( directToolServerCodes.map((serverCode) => ( {serverCode} )) )}
void handleToggleStatus(item)} aria-label={t("aiAgent.toggleStatus", { name: item.name })} /> {getStatusLabel(String(item.status), t)}
} aria-label={t("aiAgent.moreActions", { name: item.name })} > void handleToggleStatus(item)} > {item.status === Status.Ok ? t("aiAgent.stop") : t("aiAgent.enabled")} void handleDelete(item)} > {t("aiAgent.delete")}
); } export default function DashboardAIAgentsPage() { const t = useI18n(); const statusOptions = getStatusOptions(t); const [nameInput, setNameInput] = useState(""); const [statusInput, setStatusInput] = useState("all"); const [name, setName] = useState(""); const [status, setStatus] = useState("all"); const [page, setPage] = useState(1); const [limit, setLimit] = useState(20); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [actionLoadingId, setActionLoadingId] = useState(null); const [sorting, setSorting] = useState(false); const [dialogOpen, setDialogOpen] = useState(false); const [editingItemId, setEditingItemId] = useState(null); const [result, setResult] = useState>({ results: [], page: { page: 1, limit: 20, total: 0 }, }); 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 fetchAIAgents({ name: name.trim() || undefined, status: status === "all" ? undefined : status, page, limit, }); setResult(data); } catch (error) { toast.error( error instanceof Error ? error.message : t("aiAgent.loadFailed"), ); } finally { setLoading(false); } }, [limit, name, page, status, t]); useEffect(() => { void loadData(); }, [loadData]); function applyFilters() { setName(nameInput); setStatus(statusInput); setPage(1); } function handleFilterKeyDown(event: React.KeyboardEvent) { if (event.key !== "Enter") { return; } event.preventDefault(); applyFilters(); } function openCreateDialog() { setEditingItemId(null); setDialogOpen(true); } function openEditDialog(item: AIAgent) { setEditingItemId(item.id); setDialogOpen(true); } async function handleSubmit(payload: CreateAIAgentPayload) { if (saving) { return; } setSaving(true); try { if (editingItemId) { await updateAIAgent({ id: editingItemId, ...payload }); toast.success(t("aiAgent.updated", { name: payload.name })); } else { const created = await createAIAgent(payload); toast.success(t("aiAgent.created", { name: created.name })); } setDialogOpen(false); setEditingItemId(null); await loadData(); } catch (error) { toast.error( error instanceof Error ? error.message : t("aiAgent.saveFailed"), ); } finally { setSaving(false); } } async function handleToggleStatus(item: AIAgent) { setActionLoadingId(item.id); try { const nextStatus = item.status === Status.Ok ? Status.Disabled : Status.Ok; await updateAIAgentStatus(item.id, nextStatus); toast.success( t("aiAgent.statusChanged", { name: item.name, status: nextStatus === Status.Ok ? t("aiAgent.enabled") : t("aiAgent.stop"), }), ); await loadData(); } catch (error) { toast.error( error instanceof Error ? error.message : t("aiAgent.statusUpdateFailed"), ); } finally { setActionLoadingId(null); } } async function handleDelete(item: AIAgent) { setActionLoadingId(item.id); try { await deleteAIAgent(item.id); toast.success(t("aiAgent.deleted", { name: item.name })); await loadData(); } catch (error) { toast.error( error instanceof Error ? error.message : t("aiAgent.deleteFailed"), ); } finally { setActionLoadingId(null); } } async function handleDragEnd(event: DragEndEvent) { const { active, over } = event; if (!over || active.id === over.id || sorting) { return; } const previousResults = result.results; 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); setResult((current) => ({ ...current, results: nextResults, })); setSorting(true); try { await updateAIAgentSort(nextResults.map((item) => item.id)); toast.success(t("aiAgent.sortUpdated")); await loadData(); } catch (error) { setResult((current) => ({ ...current, results: previousResults, })); toast.error(error instanceof Error ? error.message : t("aiAgent.sortUpdateFailed")); } finally { setSorting(false); } } return ( <> } > setNameInput(event.target.value)} onKeyDown={handleFilterKeyDown} placeholder={t("aiAgent.filterName")} className="w-full sm:w-56" />
setPage(nextPage)} onLimitChange={(nextLimit) => { setLimit(nextLimit); setPage(1); }} /> } > Agent {t("aiAgent.columnAiConfig")} {t("aiAgent.columnServiceMode")} {t("aiAgent.columnKnowledge")} {t("aiAgent.columnSkills")} {t("aiAgent.columnCapabilities")} {t("aiAgent.columnStatus")} {t("aiAgent.columnActions")} {loading || result.results.length === 0 ? ( ) : null} item.id)} strategy={verticalListSortingStrategy} > {result.results.map((item) => ( ))}
); }