"use client"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { Button } from "@app/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@app/components/ui/dropdown-menu"; import { Switch } from "@app/components/ui/switch"; import { ControlledDataTable, type ExtendedColumnDef } from "@app/components/ui/controlled-data-table"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useNavigationContext } from "@app/hooks/useNavigationContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import type { PaginationState } from "@tanstack/react-table"; import { ArrowRight, MoreHorizontal } from "lucide-react"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useMemo, useState, useTransition } from "react"; import { useDebouncedCallback } from "use-debounce"; export type AiProviderRow = { providerId: number; niceId: string; name: string; type: string; routingMode: string; enabled: boolean; effectiveUpstreamUrl: string | null; apiKeyLastChars: string | null; }; type AiProvidersTableProps = { providers: AiProviderRow[]; orgId: string; pagination: PaginationState; rowCount: number; }; export default function AiProvidersTable({ providers, orgId, pagination, rowCount }: AiProvidersTableProps) { const router = useRouter(); const t = useTranslations(); const api = createApiClient(useEnvContext()); const { navigate: filter, isNavigating: isFiltering, searchParams } = useNavigationContext(); const [rows, setRows] = useState(providers); const [selected, setSelected] = useState(null); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isRefreshing, startTransition] = useTransition(); useEffect(() => { setRows(providers); }, [providers]); function refreshData() { startTransition(() => { try { router.refresh(); } catch { toast({ title: t("error"), description: t("refreshError"), variant: "destructive" }); } }); } const handlePaginationChange = (newPage: PaginationState) => { searchParams.set("page", (newPage.pageIndex + 1).toString()); searchParams.set("pageSize", newPage.pageSize.toString()); filter({ searchParams }); }; const handleSearchChange = useDebouncedCallback((query: string) => { searchParams.set("query", query); searchParams.delete("page"); filter({ searchParams }); }, 300); function typeLabel(type: string) { const key = `aiProviderType${type.charAt(0).toUpperCase()}${type.slice(1)}`; const map: Record = { openai: "aiProviderTypeOpenai", anthropic: "aiProviderTypeAnthropic", googleGemini: "aiProviderTypeGoogleGemini", vertexAi: "aiProviderTypeVertexAi", bedrock: "aiProviderTypeBedrock", microsoftFoundry: "aiProviderTypeMicrosoftFoundry", openRouter: "aiProviderTypeOpenRouter", vercelAiGateway: "aiProviderTypeVercelAiGateway", custom: "aiProviderTypeCustom" }; return t(map[type] ?? key); } function routingLabel(mode: string) { return mode === "target" ? t("aiProviderRoutingModeTarget") : t("aiProviderRoutingModeUrl"); } async function toggleEnabled(row: AiProviderRow, enabled: boolean) { setRows((prev) => prev.map((r) => r.providerId === row.providerId ? { ...r, enabled } : r ) ); try { await api.post(`/ai-provider/${row.providerId}`, { enabled }); toast({ title: t("success"), description: t("aiProviderUpdated") }); router.refresh(); } catch (e) { setRows((prev) => prev.map((r) => r.providerId === row.providerId ? { ...r, enabled: row.enabled } : r ) ); toast({ variant: "destructive", title: t("aiProviderErrorUpdate"), description: formatAxiosError(e, t("aiProviderErrorUpdate")) }); } } function deleteProvider(row: AiProviderRow) { startTransition(async () => { try { await api.delete(`/ai-provider/${row.providerId}`); setRows((prev) => prev.filter((r) => r.providerId !== row.providerId) ); setIsDeleteModalOpen(false); setSelected(null); toast({ title: t("success"), description: t("aiProviderDeleted") }); router.refresh(); } catch (e) { toast({ variant: "destructive", title: t("aiProviderErrorDelete"), description: formatAxiosError(e, t("aiProviderErrorDelete")) }); } }); } const columns = useMemo[]>( () => [ { accessorKey: "name", enableHiding: false, header: () => {t("name")}, cell: ({ row }) => ( {row.original.name} ) }, { accessorKey: "type", header: () => ( {t("aiProviderType")} ), cell: ({ row }) => typeLabel(row.original.type) }, { accessorKey: "routingMode", header: () => ( {t("aiProviderRoutingMode")} ), cell: ({ row }) => routingLabel(row.original.routingMode) }, { accessorKey: "effectiveUpstreamUrl", header: () => ( {t("aiProviderUpstreamUrl")} ), cell: ({ row }) => ( {row.original.effectiveUpstreamUrl ?? "-"} ) }, { accessorKey: "enabled", header: () => ( {t("aiProviderEnabled")} ), cell: ({ row }) => ( toggleEnabled(row.original, checked) } /> ) }, { id: "actions", enableHiding: false, header: () => , cell: ({ row }) => (
{t("edit")} { setSelected(row.original); setIsDeleteModalOpen(true); }} > {t("delete")}
) } ], [orgId, t] ); return ( <> {selected && ( { setIsDeleteModalOpen(val); if (!val) { setSelected(null); } }} dialog={

{t("aiProviderQuestionRemove")}

{t("aiProviderMessageRemove")}

} buttonText={t("aiProviderDeleteConfirm")} onConfirm={async () => deleteProvider(selected)} string={selected.name} title={t("aiProviderDelete")} /> )} router.push(`/${orgId}/settings/ai-providers/create`) } tableId="ai-providers-table" searchPlaceholder={t("aiProvidersSearch")} pagination={pagination} onPaginationChange={handlePaginationChange} searchQuery={searchParams.get("query")?.toString()} onSearch={handleSearchChange} onRefresh={refreshData} isRefreshing={isRefreshing || isFiltering} rowCount={rowCount} stickyRightColumn="actions" /> ); }