import { IconSearch } from "@tabler/icons-react" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useMemo, useState } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" import { type ToolSupportItem, getTools, setToolEnabled } from "@/api/tools" import { PageHeader } from "@/components/page-header" import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Skeleton } from "@/components/ui/skeleton" import { Switch } from "@/components/ui/switch" import { cn } from "@/lib/utils" import { refreshGatewayState } from "@/store/gateway" export function ToolsPage() { const { t } = useTranslation() const queryClient = useQueryClient() const { data, isLoading, error } = useQuery({ queryKey: ["tools"], queryFn: getTools, }) const [searchQuery, setSearchQuery] = useState("") const [statusFilter, setStatusFilter] = useState("all") const toggleMutation = useMutation({ mutationFn: async ({ name, enabled }: { name: string; enabled: boolean }) => setToolEnabled(name, enabled), onSuccess: (_, variables) => { toast.success( variables.enabled ? t("pages.agent.tools.enable_success") : t("pages.agent.tools.disable_success"), ) void queryClient.invalidateQueries({ queryKey: ["tools"] }) void refreshGatewayState({ force: true }) }, onError: (err) => { toast.error( err instanceof Error ? err.message : t("pages.agent.tools.toggle_error"), ) }, }) // Filter and group tools const { groupedTools, totalFilteredCount } = useMemo(() => { if (!data) return { groupedTools: [], totalFilteredCount: 0 } let count = 0 const buckets = new Map() for (const item of data.tools) { // Apply status filter if (statusFilter !== "all" && item.status !== statusFilter) continue // Apply search query if (searchQuery.trim()) { const query = searchQuery.toLowerCase() const matchesName = item.name.toLowerCase().includes(query) const matchesDesc = (item.description || "") .toLowerCase() .includes(query) if (!matchesName && !matchesDesc) continue } count++ const list = buckets.get(item.category) ?? [] list.push(item) buckets.set(item.category, list) } return { groupedTools: Array.from(buckets.entries()), totalFilteredCount: count, } }, [data, searchQuery, statusFilter]) return (
{/* Header & Description */}
{/* Filters Toolbar */}
setSearchQuery(e.target.value)} />
{/* Content Area */} {error ? (

{t("pages.agent.load_error")}

) : isLoading ? ( // Skeleton Loading State
{[1, 2].map((groupIndex) => (
{[1, 2, 3, 4].map((itemIndex) => ( ))}
))}
) : totalFilteredCount === 0 ? ( // Empty State

{data?.tools.length === 0 ? t("pages.agent.tools.empty") : t("pages.agent.tools.no_results")}

{data?.tools.length !== 0 && (

Try adjusting your search criteria or status filters.

)}
) : ( // Tool Categories list
{groupedTools.map(([category, items]) => (

{t(`pages.agent.tools.categories.${category}`)}

{items.map((tool) => { const reasonText = tool.reason_code ? t(`pages.agent.tools.reasons.${tool.reason_code}`) : "" const isPending = toggleMutation.isPending && toggleMutation.variables?.name === tool.name const isEnabled = tool.status === "enabled" const isDisabled = tool.status === "disabled" const isBlocked = tool.status === "blocked" return (
{tool.name}
{tool.description}
toggleMutation.mutate({ name: tool.name, enabled: checked, }) } />
{reasonText && (
{reasonText}
)}
) })}
))}
)}
) } function ToolStatusBadge({ status }: { status: ToolSupportItem["status"] }) { const { t } = useTranslation() return ( {t(`pages.agent.tools.status.${status}`)} ) }