import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { MoreHorizontal, Pencil, Plus, RefreshCw, Search, Trash2 } from "lucide-react"; import { useState } from "react"; import { useForm, useWatch } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { z } from "zod"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Spinner } from "@/components/ui/spinner"; import { Table, TableActionCell, TableActionHead, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { createModel, deleteModel, deleteModels, listModelAccountOptions, listModels, syncModels, updateModel, updateModelsEnabled } from "@/entities/model/model-api"; import type { ModelRouteDTO } from "@/entities/model/types"; import { EmptyState, ErrorState, TableLoadingRow } from "@/shared/components/data-state"; import { DataTableShell } from "@/shared/components/data-table-shell"; import { DataTableFilters } from "@/shared/components/data-table-filters"; import { Pagination } from "@/shared/components/pagination"; import { SortableTableHead } from "@/shared/components/sortable-table-head"; import { VirtualTableBody } from "@/shared/components/virtual-table-body"; import { useDebouncedValue } from "@/shared/hooks/use-debounced-value"; import { cn } from "@/shared/lib/cn"; import { formatDateTime } from "@/shared/lib/format"; import { nextTableSort, type SortOrder, type TableSort } from "@/shared/lib/table-sort"; export function ModelsPage() { const { t, i18n } = useTranslation(); const queryClient = useQueryClient(); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(20); const [search, setSearch] = useState(""); const [statusFilter, setStatusFilter] = useState(""); const [providerFilter, setProviderFilter] = useState(""); const [sort, setSort] = useState({ field: "", order: "asc" }); const [selected, setSelected] = useState>(() => new Set()); const [editing, setEditing] = useState(null); const [deleting, setDeleting] = useState(null); const [batchDeleteOpen, setBatchDeleteOpen] = useState(false); const [accountSearch, setAccountSearch] = useState(""); const debouncedSearch = useDebouncedValue(search); const schema = z.object({ publicId: z.string().min(1, t("errors.required")), provider: z.enum(["grok_build", "grok_web", "grok_console"]), upstreamModel: z.string().min(1, t("errors.required")), capability: z.enum(["responses", "chat", "image", "image_edit", "video"]), enabled: z.boolean(), bindingMode: z.boolean(), accountIds: z.array(z.string()), }).refine((value) => !value.bindingMode || value.accountIds.length > 0, { path: ["accountIds"], message: t("models.selectAccountRequired") }); type ModelForm = z.infer; const form = useForm({ resolver: zodResolver(schema), defaultValues: { publicId: "", provider: "grok_build", upstreamModel: "", capability: "responses", enabled: true, bindingMode: false, accountIds: [] }, }); const modelEnabled = useWatch({ control: form.control, name: "enabled" }); const selectedProvider = useWatch({ control: form.control, name: "provider" }); const selectedCapability = useWatch({ control: form.control, name: "capability" }); const bindingMode = useWatch({ control: form.control, name: "bindingMode" }); const selectedAccountIDs = useWatch({ control: form.control, name: "accountIds" }); const modelsQuery = useQuery({ queryKey: ["models", page, pageSize, debouncedSearch, statusFilter, providerFilter, sort.field, sort.order], queryFn: () => listModels({ page, pageSize, search: debouncedSearch, status: statusFilter, provider: providerFilter, sortBy: sort.field || undefined, sortOrder: sort.field ? sort.order : undefined }), }); const accountOptionsQuery = useQuery({ queryKey: ["models", "account-options", selectedProvider], queryFn: () => listModelAccountOptions(selectedProvider), enabled: editing !== null, }); const updateMutation = useMutation({ mutationFn: (values: ModelForm) => { if (!editing) throw new Error(t("errors.generic")); const input = { ...values, accountIds: values.bindingMode ? values.accountIds : [] }; if (editing === "new") return createModel(input); return updateModel(editing.id, { publicId: input.publicId, enabled: input.enabled, accountIds: input.accountIds }); }, onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["models"] }); setEditing(null); toast.success(t(editing === "new" ? "models.created" : "models.updated")); }, onError: showError, }); const deleteMutation = useMutation({ mutationFn: (id: string) => deleteModel(id), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["models"] }); setDeleting(null); toast.success(t("models.deleted")); }, onError: showError, }); const batchDeleteMutation = useMutation({ mutationFn: () => deleteModels([...selected]), onSuccess: (result) => { setSelected(new Set()); setBatchDeleteOpen(false); void queryClient.invalidateQueries({ queryKey: ["models"] }); toast.success(t("models.batchDeleted", { count: result.deleted })); }, onError: showError, }); const batchUpdateMutation = useMutation({ mutationFn: (enabled: boolean) => updateModelsEnabled([...selected], enabled), onSuccess: () => { setSelected(new Set()); void queryClient.invalidateQueries({ queryKey: ["models"] }); toast.success(t("models.batchUpdated")); }, onError: showError, }); const syncMutation = useMutation({ mutationFn: syncModels, onSuccess: (result) => { void queryClient.invalidateQueries({ queryKey: ["models"] }); toast.success(t("models.synced", { count: result.synced })); }, onError: showError, }); function showError(error: unknown): void { toast.error(error instanceof Error ? error.message : t("errors.generic")); } function beginEdit(model: ModelRouteDTO): void { setEditing(model); setAccountSearch(""); form.reset({ publicId: model.publicId, provider: model.provider, upstreamModel: model.upstreamModel, capability: model.capability, enabled: model.enabled, bindingMode: model.bindingMode, accountIds: model.accountIds, }); } function beginCreate(): void { setEditing("new"); setAccountSearch(""); form.reset({ publicId: "", provider: "grok_build", upstreamModel: "", capability: "responses", enabled: true, bindingMode: false, accountIds: [] }); } function toggleBoundAccount(id: string, checked: boolean): void { const current = form.getValues("accountIds"); form.setValue("accountIds", checked ? [...new Set([...current, id])] : current.filter((value) => value !== id), { shouldValidate: true }); } const accountOptions = accountOptionsQuery.data?.items ?? []; const normalizedAccountSearch = accountSearch.trim().toLocaleLowerCase(); const visibleAccountOptions = normalizedAccountSearch ? accountOptions.filter((account) => account.name.toLocaleLowerCase().includes(normalizedAccountSearch) || account.id.includes(normalizedAccountSearch)) : accountOptions; const result = modelsQuery.data; const pageIDs = result?.items.map((model) => model.id) ?? []; const selectedOnPage = pageIDs.filter((id) => selected.has(id)); const allPageSelected = pageIDs.length > 0 && selectedOnPage.length === pageIDs.length; function togglePage(checked: boolean): void { setSelected((current) => { const next = new Set(current); for (const id of pageIDs) { if (checked) next.add(id); else next.delete(id); } return next; }); } function toggleModel(id: string, checked: boolean): void { setSelected((current) => { const next = new Set(current); if (checked) next.add(id); else next.delete(id); return next; }); } function changeSort(field: string, initialOrder: SortOrder): void { setSort((current) => nextTableSort(current, field, initialOrder)); setPage(1); } return (

{t("models.title")}

{t("models.description")}

{ setSearch(event.target.value); setPage(1); }} placeholder={t("models.search")} aria-label={t("models.search")} />
{ setProviderFilter(value as ModelRouteDTO["provider"] | ""); setPage(1); }, options: [ { value: "grok_build", label: t("models.providerGrokBuild") }, { value: "grok_web", label: t("models.providerGrokWeb") }, { value: "grok_console", label: t("console.name") }, ] }, { id: "status", label: t("models.status"), value: statusFilter, onChange: (value) => { setStatusFilter(value); setPage(1); }, options: [ { value: "enabled", label: t("common.enabled") }, { value: "disabled", label: t("common.disabled") }, ] }, ]} />
{selected.size > 0 ? ( <> {t("common.selectedCount", { count: selected.size })} ) : null}
)} footer={result && result.total > 0 ? { setPageSize(value); setPage(1); }} /> : undefined} > {modelsQuery.isError ? void modelsQuery.refetch()} /> : null} {result && result.items.length === 0 ? : null} {modelsQuery.isPending || (result && result.items.length > 0) ? ( 0 ? "indeterminate" : false} onCheckedChange={(checked) => togglePage(checked === true)} aria-label={t("common.selectPage")} /> {t("models.model")} {t("models.upstream")} {t("models.status")} {t("models.provider")} {t("models.accountSupport")} {t("models.lastSyncedAt")} {modelsQuery.isPending ? ( ) : ( ( toggleModel(model.id, checked === true)} aria-label={t("common.selectItem", { name: model.publicId })} /> {model.publicId} {model.upstreamModel} {model.enabled ? {t("common.enabled")} : {t("common.disabled")}}
0 ? "text-emerald-600 dark:text-emerald-400" : "text-muted-foreground")}>{model.supportedAccounts}/ {model.totalAccounts} {model.bindingMode ? {t("models.boundAccounts")} : null}
{formatDateTime(model.lastSyncedAt, i18n.language)} beginEdit(model)}>{t("common.edit")} setDeleting(model)}>{t("common.delete")}
)} /> )}
) : null}
!open && setEditing(null)}> {t(editing === "new" ? "models.createTitle" : "models.editTitle")} {editing === "new" ? t("models.createDescription") : editing?.upstreamModel}
updateMutation.mutate(values))}>
{form.formState.errors.publicId ?

{form.formState.errors.publicId.message}

: null}
{editing === "new" ? (
{form.formState.errors.upstreamModel ?

{form.formState.errors.upstreamModel.message}

: null}
) : null}
{bindingMode ? {t("models.selectedAccounts", { count: selectedAccountIDs.length })} : null}

{t("models.bindAccountsDescription")}

{ form.setValue("bindingMode", checked); if (!checked) form.clearErrors("accountIds"); }} />
{bindingMode ? (
setAccountSearch(event.target.value)} placeholder={t("models.searchAccounts")} />
{accountOptionsQuery.isPending ?
: null} {accountOptionsQuery.isError ?

{accountOptionsQuery.error.message}

: null} {!accountOptionsQuery.isPending && visibleAccountOptions.length === 0 ?

{t("models.noBindableAccounts")}

: null} {visibleAccountOptions.map((account) => { const controlId = `model-account-${account.id}`; const checked = selectedAccountIDs.includes(account.id); return ( ); })}
{form.formState.errors.accountIds ?

{form.formState.errors.accountIds.message}

: null}
) : null}

{t("models.enabledDescription")}

form.setValue("enabled", checked)} />
!open && setDeleting(null)}> {t("models.deleteTitle")}{t("models.deleteDescription", { name: deleting?.publicId ?? "" })} {t("common.cancel")} deleting && deleteMutation.mutate(deleting.id)}>{deleteMutation.isPending ? : null}{t("common.delete")} {t("models.batchDeleteTitle", { count: selected.size })}{t("models.batchDeleteDescription")} {t("common.cancel")} batchDeleteMutation.mutate()}>{batchDeleteMutation.isPending ? : null}{t("common.delete")}
); } function ModelProvider({ provider }: { provider: ModelRouteDTO["provider"] }) { const { t } = useTranslation(); const label = provider === "grok_web" ? t("models.providerGrokWeb") : provider === "grok_console" ? t("console.name") : t("models.providerGrokBuild"); const color = provider === "grok_web" ? "bg-quota-product-2" : provider === "grok_console" ? "bg-quota-product-4" : "bg-quota-product-1"; return ( {label} ); }