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 <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 09:49:15 +00:00
parent 1a987d9a90
commit eaa075ee8e
No known key found for this signature in database
2 changed files with 156 additions and 89 deletions

View file

@ -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",

View file

@ -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 (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => onCopy(value)}
className="cursor-pointer text-muted-foreground hover:text-primary"
aria-label={title}
>
<Copy size={12} />
</button>
</TooltipTrigger>
<TooltipContent>{title}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
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 (
<div className="space-y-1">
<div className="flex items-center space-x-2">
<Text className="font-medium text-sm">{model.model_group}</Text>
<Tooltip title="Copy model name">
<CopyOutlined
onClick={() => copyToClipboard(model.model_group)}
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs"
/>
</Tooltip>
<span className="font-medium text-sm">{model.model_group}</span>
<CopyIconButton
value={model.model_group}
onCopy={copyToClipboard}
title="Copy model name"
/>
</div>
{/* Show provider on mobile when provider column is hidden */}
<div className="md:hidden">
<Text className="text-xs text-gray-600">{model.providers.join(", ")}</Text>
<span className="text-xs text-muted-foreground">
{model.providers.join(", ")}
</span>
</div>
</div>
);
@ -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 (
<div className="flex flex-wrap gap-1">
{model.providers.slice(0, 2).map((provider) => (
<Tag key={provider} color="blue" className="text-xs">
<Badge
key={provider}
variant="outline"
className="text-xs bg-blue-100 text-blue-800 dark:bg-blue-950/40 dark:text-blue-300 border-blue-200 dark:border-blue-800"
>
{provider}
</Tag>
</Badge>
))}
{model.providers.length > 2 && <Text className="text-xs text-gray-500">+{model.providers.length - 2}</Text>}
{model.providers.length > 2 && (
<span className="text-xs text-muted-foreground">
+{model.providers.length - 2}
</span>
)}
</div>
);
},
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 ? (
<Badge color="green" size="sm">
<Badge
variant="outline"
className="text-xs bg-emerald-100 text-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-300 border-emerald-200 dark:border-emerald-800"
>
{model.mode}
</Badge>
) : (
<Text className="text-gray-500">-</Text>
<span className="text-muted-foreground">-</span>
);
},
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 (
<div className="space-y-1">
<Text className="text-xs">
{model.max_input_tokens ? formatTokens(model.max_input_tokens) : "-"} /{" "}
{model.max_output_tokens ? formatTokens(model.max_output_tokens) : "-"}
</Text>
<span className="text-xs">
{model.max_input_tokens
? formatTokens(model.max_input_tokens)
: "-"}{" "}
/{" "}
{model.max_output_tokens
? formatTokens(model.max_output_tokens)
: "-"}
</span>
</div>
);
},
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 (
<div className="space-y-1">
<Text className="text-xs">{model.input_cost_per_token ? formatCost(model.input_cost_per_token) : "-"}</Text>
<Text className="text-xs text-gray-500">
{model.output_cost_per_token ? formatCost(model.output_cost_per_token) : "-"}
</Text>
<span className="text-xs block">
{model.input_cost_per_token
? formatCost(model.input_cost_per_token)
: "-"}
</span>
<span className="text-xs text-muted-foreground block">
{model.output_cost_per_token
? formatCost(model.output_cost_per_token)
: "-"}
</span>
</div>
);
},
@ -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 (
<div className="flex flex-wrap gap-1">
{capabilities.length === 0 ? (
<Text className="text-gray-500 text-xs">-</Text>
<span className="text-muted-foreground text-xs">-</span>
) : (
capabilities.map((capability, index) => (
<Badge key={capability} color={colors[index % colors.length]} size="xs">
<Badge
key={capability}
variant="outline"
className={cn(
"text-xs",
capabilityColors[index % capabilityColors.length],
)}
>
{formatCapabilityName(capability)}
</Badge>
))
@ -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 ? (
<Badge color="green" size="xs">
<Badge variant="default" className="text-xs">
Yes
</Badge>
) : (
<Badge color="gray" size="xs">
<Badge variant="outline" className="text-xs">
No
</Badge>
);
},
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 (
<Button size="xs" variant="secondary" onClick={() => showModal(model)} icon={InfoCircleOutlined}>
<span className="hidden lg:inline">Details</span>
<span className="lg:hidden">Info</span>
</Button>
);
},
cell: ({ row }) => (
<Button
size="sm"
variant="secondary"
onClick={() => showModal(row.original)}
>
<Info className="h-3 w-3" />
<span className="hidden lg:inline">Details</span>
<span className="lg:hidden">Info</span>
</Button>
),
},
];
// 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;
});
}