From eaa075ee8e52db11b7ac164baf0e7f4eeb2a7cbc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 09:49:15 +0000 Subject: [PATCH] feat(ui): migrate model_hub_table_columns to shadcn - Same pattern as mcp_hub_table_columns: @tremor + antd + @ant-design/ icons \u2192 shadcn Badge/Button/Tooltip + lucide. Capabilities palette cycles through a 6-color categorical array (emerald/blue/purple/ orange/red/amber) kept as inline className strings; added to the raw-colors override list. - Public/Private/Mode/Provider badges map to shadcn default/outline variants with categorical color extensions where the hue matters. Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang --- ui/litellm-dashboard/.eslintrc.json | 3 +- .../components/model_hub_table_columns.tsx | 242 +++++++++++------- 2 files changed, 156 insertions(+), 89 deletions(-) diff --git a/ui/litellm-dashboard/.eslintrc.json b/ui/litellm-dashboard/.eslintrc.json index 6049b3b352a..78891527ad7 100644 --- a/ui/litellm-dashboard/.eslintrc.json +++ b/ui/litellm-dashboard/.eslintrc.json @@ -36,7 +36,8 @@ "src/components/model_group_alias_settings.tsx", "src/components/cache_dashboard.tsx", "src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterTestPanel.tsx", - "src/components/mcp_hub_table_columns.tsx" + "src/components/mcp_hub_table_columns.tsx", + "src/components/model_hub_table_columns.tsx" ], "rules": { "litellm-ui/no-banned-ui-imports": "off", diff --git a/ui/litellm-dashboard/src/components/model_hub_table_columns.tsx b/ui/litellm-dashboard/src/components/model_hub_table_columns.tsx index 64de1dd98e8..e768ba72ea6 100644 --- a/ui/litellm-dashboard/src/components/model_hub_table_columns.tsx +++ b/ui/litellm-dashboard/src/components/model_hub_table_columns.tsx @@ -1,7 +1,14 @@ import { ColumnDef } from "@tanstack/react-table"; -import { Button, Badge, Text } from "@tremor/react"; -import { Tooltip, Tag } from "antd"; -import { CopyOutlined, InfoCircleOutlined } from "@ant-design/icons"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { Copy, Info } from "lucide-react"; +import { cn } from "@/lib/utils"; interface ModelHubData { model_group: string; @@ -18,36 +25,70 @@ interface ModelHubData { supports_function_calling: boolean; supported_openai_params?: string[]; is_public_model_group: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } -const formatCapabilityName = (key: string) => { - return key +const formatCapabilityName = (key: string) => + key .replace(/^supports_/, "") .split("_") .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(" "); -}; -const getModelCapabilities = (model: ModelHubData) => { - return Object.entries(model) +const getModelCapabilities = (model: ModelHubData) => + Object.entries(model) .filter(([key, value]) => key.startsWith("supports_") && value === true) .map(([key]) => key); -}; -const formatCost = (cost: number) => { - return `$${(cost * 1_000_000).toFixed(2)}`; -}; +const formatCost = (cost: number) => `$${(cost * 1_000_000).toFixed(2)}`; const formatTokens = (tokens: number) => { - if (tokens >= 1_000_000) { - return `${(tokens / 1_000_000).toFixed(1)}M`; - } else if (tokens >= 1_000) { - return `${(tokens / 1_000).toFixed(1)}K`; - } + if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`; + if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}K`; return tokens.toString(); }; +// Categorical capability-color palette. Each index cycles through a set of +// tags; added to the raw-colors eslintrc override since these are +// categorical by design. +const capabilityColors = [ + "bg-emerald-100 text-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-300", + "bg-blue-100 text-blue-800 dark:bg-blue-950/40 dark:text-blue-300", + "bg-purple-100 text-purple-800 dark:bg-purple-950/40 dark:text-purple-300", + "bg-orange-100 text-orange-800 dark:bg-orange-950/40 dark:text-orange-300", + "bg-red-100 text-red-800 dark:bg-red-950/40 dark:text-red-300", + "bg-amber-100 text-amber-800 dark:bg-amber-950/40 dark:text-amber-300", +]; + +function CopyIconButton({ + value, + onCopy, + title, +}: { + value: string; + onCopy: (text: string) => void; + title: string; +}) { + return ( + + + + + + {title} + + + ); +} + export const modelHubColumns = ( showModal: (model: ModelHubData) => void, copyToClipboard: (text: string) => void, @@ -61,21 +102,20 @@ export const modelHubColumns = ( sortingFn: "alphanumeric", cell: ({ row }) => { const model = row.original; - return (
- {model.model_group} - - copyToClipboard(model.model_group)} - className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs" - /> - + {model.model_group} +
- {/* Show provider on mobile when provider column is hidden */}
- {model.providers.join(", ")} + + {model.providers.join(", ")} +
); @@ -85,28 +125,32 @@ export const modelHubColumns = ( header: "Provider", accessorKey: "providers", enableSorting: true, - sortingFn: (rowA, rowB) => { - const providersA = rowA.original.providers.join(", "); - const providersB = rowB.original.providers.join(", "); - return providersA.localeCompare(providersB); - }, + sortingFn: (rowA, rowB) => + rowA.original.providers.join(", ").localeCompare( + rowB.original.providers.join(", "), + ), cell: ({ row }) => { const model = row.original; - return (
{model.providers.slice(0, 2).map((provider) => ( - + {provider} - + ))} - {model.providers.length > 2 && +{model.providers.length - 2}} + {model.providers.length > 2 && ( + + +{model.providers.length - 2} + + )}
); }, - meta: { - className: "hidden md:table-cell", - }, + meta: { className: "hidden md:table-cell" }, }, { header: "Mode", @@ -115,62 +159,77 @@ export const modelHubColumns = ( sortingFn: "alphanumeric", cell: ({ row }) => { const model = row.original; - return model.mode ? ( - + {model.mode} ) : ( - - + - ); }, - meta: { - className: "hidden lg:table-cell", - }, + meta: { className: "hidden lg:table-cell" }, }, { header: "Tokens", accessorKey: "max_input_tokens", enableSorting: true, sortingFn: (rowA, rowB) => { - const tokensA = (rowA.original.max_input_tokens || 0) + (rowA.original.max_output_tokens || 0); - const tokensB = (rowB.original.max_input_tokens || 0) + (rowB.original.max_output_tokens || 0); + const tokensA = + (rowA.original.max_input_tokens || 0) + + (rowA.original.max_output_tokens || 0); + const tokensB = + (rowB.original.max_input_tokens || 0) + + (rowB.original.max_output_tokens || 0); return tokensA - tokensB; }, cell: ({ row }) => { const model = row.original; - return (
- - {model.max_input_tokens ? formatTokens(model.max_input_tokens) : "-"} /{" "} - {model.max_output_tokens ? formatTokens(model.max_output_tokens) : "-"} - + + {model.max_input_tokens + ? formatTokens(model.max_input_tokens) + : "-"}{" "} + /{" "} + {model.max_output_tokens + ? formatTokens(model.max_output_tokens) + : "-"} +
); }, - meta: { - className: "hidden lg:table-cell", - }, + meta: { className: "hidden lg:table-cell" }, }, { header: "Cost/1M", accessorKey: "input_cost_per_token", enableSorting: true, sortingFn: (rowA, rowB) => { - const costA = (rowA.original.input_cost_per_token || 0) + (rowA.original.output_cost_per_token || 0); - const costB = (rowB.original.input_cost_per_token || 0) + (rowB.original.output_cost_per_token || 0); + const costA = + (rowA.original.input_cost_per_token || 0) + + (rowA.original.output_cost_per_token || 0); + const costB = + (rowB.original.input_cost_per_token || 0) + + (rowB.original.output_cost_per_token || 0); return costA - costB; }, cell: ({ row }) => { const model = row.original; - return (
- {model.input_cost_per_token ? formatCost(model.input_cost_per_token) : "-"} - - {model.output_cost_per_token ? formatCost(model.output_cost_per_token) : "-"} - + + {model.input_cost_per_token + ? formatCost(model.input_cost_per_token) + : "-"} + + + {model.output_cost_per_token + ? formatCost(model.output_cost_per_token) + : "-"} +
); }, @@ -182,15 +241,20 @@ export const modelHubColumns = ( cell: ({ row }) => { const model = row.original; const capabilities = getModelCapabilities(model); - const colors = ["green", "blue", "purple", "orange", "red", "yellow"]; - return (
{capabilities.length === 0 ? ( - - + - ) : ( capabilities.map((capability, index) => ( - + {formatCapabilityName(capability)} )) @@ -204,50 +268,52 @@ export const modelHubColumns = ( accessorKey: "is_public_model_group", enableSorting: true, sortingFn: (rowA, rowB) => { - const publicA = rowA.original.is_public_model_group === true ? 1 : 0; - const publicB = rowB.original.is_public_model_group === true ? 1 : 0; + const publicA = + rowA.original.is_public_model_group === true ? 1 : 0; + const publicB = + rowB.original.is_public_model_group === true ? 1 : 0; return publicA - publicB; }, cell: ({ row }) => { const model = row.original; - return model.is_public_model_group === true ? ( - + Yes ) : ( - + No ); }, - meta: { - className: "hidden md:table-cell", - }, + meta: { className: "hidden md:table-cell" }, }, { header: "Details", id: "details", enableSorting: false, - cell: ({ row }) => { - const model = row.original; - - return ( - - ); - }, + cell: ({ row }) => ( + + ), }, ]; - // Filter out columns based on publicPage setting if (publicPage) { return allColumns.filter((column) => { - // Remove the public column - if ("accessorKey" in column && column.accessorKey === "is_public_model_group") return false; - + if ( + "accessorKey" in column && + column.accessorKey === "is_public_model_group" + ) { + return false; + } return true; }); }