"use client" import { useCallback, useEffect, useMemo, useState } from "react" import { BrainCircuitIcon, BugIcon, MoreHorizontalIcon, PlusIcon, RefreshCwIcon, RotateCcwIcon, SearchIcon, Trash2Icon, } from "lucide-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 { ButtonGroup } from "@/components/ui/button-group" 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 { createSkillDefinition, deleteSkillDefinition, fetchSkillDefinitions, restoreSkillDefinition, updateSkillDefinition, updateSkillDefinitionStatus, type CreateSkillDefinitionPayload, type PageResult, type SkillDefinition, } from "@/lib/api/admin" import { useI18n } from "@/i18n/provider" import { Status } from "@/lib/generated/enums" import { formatDateTime } from "@/lib/utils" import { EditDialog } from "./_components/edit" import { DebugDialog } from "./_components/debug-dialog" type TFunction = (key: string, values?: Record) => string function statusLabel(status: number, t: TFunction) { if (status === Status.Ok) { return t("skillDefinition.statusOk") } if (status === Status.Disabled) { return t("skillDefinition.statusDisabled") } if (status === Status.Deleted) { return t("skillDefinition.statusDeleted") } return String(status) } function getStatusFilterOptions(t: TFunction) { return [ { value: "all", label: t("skillDefinition.allStatus") }, { value: String(Status.Ok), label: t("skillDefinition.statusOk") }, { value: String(Status.Disabled), label: t("skillDefinition.statusDisabled") }, { value: String(Status.Deleted), label: t("skillDefinition.statusDeleted") }, ] } type SkillRowProps = { item: SkillDefinition actionLoadingId: number | null openEditDialog: (item: SkillDefinition) => void openDebugDialog: (item: SkillDefinition) => void handleToggleStatus: (item: SkillDefinition) => void handleDelete: (item: SkillDefinition) => void handleRestore: (item: SkillDefinition) => void t: TFunction } function SkillRow({ item, actionLoadingId, openEditDialog, openDebugDialog, handleToggleStatus, handleDelete, handleRestore, t, }: SkillRowProps) { const isDeleted = item.status === Status.Deleted const statusBadgeVariant = isDeleted ? "destructive" : item.status === Status.Ok ? "default" : "outline" return (
{item.name}
{item.code} {t("skillDefinition.whitelistCount", { count: item.toolWhitelist.length })} {t("skillDefinition.exampleCount", { count: item.examples.length })}
{item.description || t("skillDefinition.noDescription")}
{item.toolWhitelist.length > 0 ? (
{item.toolWhitelist.slice(0, 3).map((toolCode) => ( {toolCode} ))} {item.toolWhitelist.length > 3 ? ( +{item.toolWhitelist.length - 3} ) : null}
) : null}
void handleToggleStatus(item)} aria-label={t("skillDefinition.toggleStatus", { name: item.name })} /> {statusLabel(item.status, t)}
{formatDateTime(item.updatedAt)}
{item.updateUserName || "-"}
} aria-label={t("skillDefinition.moreActions", { name: item.name })} > {isDeleted ? ( void handleRestore(item)} > {actionLoadingId === item.id ? t("skillDefinition.restoring") : t("skillDefinition.restore")} ) : ( void handleDelete(item)} className="text-destructive focus:text-destructive" > {actionLoadingId === item.id ? t("skillDefinition.deleting") : t("skillDefinition.delete")} )}
) } export default function DashboardSkillsPage() { const t = useI18n() const [nameInput, setNameInput] = useState("") const [codeInput, setCodeInput] = useState("") const [statusFilterInput, setStatusFilterInput] = useState("all") const [name, setName] = useState("") const [code, setCode] = useState("") const [statusFilter, setStatusFilter] = 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 [dialogOpen, setDialogOpen] = useState(false) const [debugDialogOpen, setDebugDialogOpen] = useState(false) const [editingItem, setEditingItem] = useState(null) const [debuggingItem, setDebuggingItem] = useState(null) const [result, setResult] = useState>({ results: [], page: { page: 1, limit: 20, total: 0 }, }) const statusFilterOptions = useMemo(() => getStatusFilterOptions(t), [t]) const loadData = useCallback(async () => { setLoading(true) try { const data = await fetchSkillDefinitions({ name: name.trim() || undefined, code: code.trim() || undefined, status: statusFilter === "all" ? undefined : Number(statusFilter), page, limit, }) setResult(data) } catch (error) { toast.error(error instanceof Error ? error.message : t("skillDefinition.loadFailed")) } finally { setLoading(false) } }, [name, code, statusFilter, page, limit, t]) useEffect(() => { void loadData() }, [loadData]) function applyFilters() { setName(nameInput) setCode(codeInput) setStatusFilter(statusFilterInput) setPage(1) } function handleFilterKeyDown(event: React.KeyboardEvent) { if (event.key !== "Enter") { return } event.preventDefault() applyFilters() } function handlePageChange(nextPage: number) { if (nextPage < 1 || nextPage === page) { return } setPage(nextPage) } function openCreateDialog() { setEditingItem(null) setDialogOpen(true) } function openEditDialog(item: SkillDefinition) { setEditingItem(item) setDialogOpen(true) } function openDebugDialog(item: SkillDefinition) { setDebuggingItem(item) setDebugDialogOpen(true) } function handleDialogOpenChange(open: boolean) { if (saving) { return } if (!open) { setEditingItem(null) } setDialogOpen(open) } function handleDebugDialogOpenChange(open: boolean) { if (!open) { setDebuggingItem(null) } setDebugDialogOpen(open) } async function handleSubmit(payload: CreateSkillDefinitionPayload) { if (saving) { return } setSaving(true) try { if (editingItem) { await updateSkillDefinition({ id: editingItem.id, ...payload, }) toast.success(t("skillDefinition.updated", { name: editingItem.name })) } else { await createSkillDefinition(payload) toast.success(t("skillDefinition.created", { name: payload.name })) } setDialogOpen(false) setEditingItem(null) await loadData() } catch (error) { toast.error(error instanceof Error ? error.message : t("skillDefinition.saveFailed")) } finally { setSaving(false) } } async function handleToggleStatus(item: SkillDefinition) { if (item.status === Status.Deleted) { return } const nextStatus = item.status === Status.Ok ? Status.Disabled : Status.Ok setActionLoadingId(item.id) try { await updateSkillDefinitionStatus(item.id, nextStatus) toast.success(t(nextStatus === Status.Ok ? "skillDefinition.enabled" : "skillDefinition.disabled", { name: item.name })) await loadData() } catch (error) { toast.error(error instanceof Error ? error.message : t("skillDefinition.statusUpdateFailed")) } finally { setActionLoadingId(null) } } async function handleDelete(item: SkillDefinition) { if (item.status === Status.Deleted) { return } setActionLoadingId(item.id) try { await deleteSkillDefinition(item.id) toast.success(t("skillDefinition.deleted", { name: item.name })) await loadData() } catch (error) { toast.error(error instanceof Error ? error.message : t("skillDefinition.deleteFailed")) } finally { setActionLoadingId(null) } } async function handleRestore(item: SkillDefinition) { if (item.status !== Status.Deleted) { return } setActionLoadingId(item.id) try { await restoreSkillDefinition(item.id) toast.success(t("skillDefinition.restored", { name: item.name })) await loadData() } catch (error) { toast.error(error instanceof Error ? error.message : t("skillDefinition.restoreFailed")) } finally { setActionLoadingId(null) } } return ( <> } >
setNameInput(event.target.value)} onKeyDown={handleFilterKeyDown} placeholder={t("skillDefinition.filterName")} className="pl-9" />
setCodeInput(event.target.value)} onKeyDown={handleFilterKeyDown} placeholder={t("skillDefinition.filterCode")} className="w-full sm:w-56" />
{ setLimit(nextLimit) setPage(1) }} /> } > Skill {t("skillDefinition.status")} {t("skillDefinition.updatedAt")} {t("skillDefinition.actions")} {loading || result.results.length === 0 ? ( ) : null} {result.results.map((item) => ( ))}
) }