feat(ui): agents \u2014 AgentCard + AgentCardGrid + AgentCostView to shadcn

- AgentCard: antd Card/Badge/Tooltip/Button + @ant-design/icons Copy/
  Key/Warning/Delete \u2192 shadcn Card/Tooltip/Button + lucide Copy/Key/
  AlertTriangle/Trash2. Status dot (emerald/amber) is categorical and
  the file is added to .eslintrc.json overrides.
- AgentCardGrid: antd Skeleton \u2192 shadcn Skeleton. Empty state uses
  border-border + bg-muted/50.
- AgentCostView: @tremor Title + antd Descriptions \u2192 plain semantic
  two-column grid with bg-muted header column.
- cost_config_fields.tsx skipped \u2014 the form is still driven by antd
  Form.Item from the parent; defer to when the full Agents form is
  migrated.

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 10:52:21 +00:00
parent 08b1c0b897
commit e00efbf810
No known key found for this signature in database
4 changed files with 113 additions and 66 deletions

View file

@ -47,7 +47,9 @@
"src/components/skill_hub_table_columns.tsx",
"src/components/common_components/PassThroughSecuritySection.tsx",
"src/components/common_components/DeleteResourceModal.tsx",
"src/components/common_components/PremiumLoggingSettings.tsx"
"src/components/common_components/PremiumLoggingSettings.tsx",
"src/components/common_components/AutoRotationView.tsx",
"src/components/agents/agent_card.tsx"
],
"rules": {
"litellm-ui/no-banned-ui-imports": "off",

View file

@ -1,6 +1,13 @@
import React from "react";
import { Card, Badge, Tooltip, Button } from "antd";
import { CopyOutlined, KeyOutlined, WarningOutlined, DeleteOutlined } from "@ant-design/icons";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { AlertTriangle, Copy, Key, Trash2 } from "lucide-react";
import { Agent, AgentKeyInfo } from "./types";
interface AgentCardProps {
@ -24,10 +31,17 @@ const AgentCard: React.FC<AgentCardProps> = ({
agent.agent_card_params?.description || "No description";
const url = agent.agent_card_params?.url;
const hasKey = keyInfo?.has_key ?? false;
const statusBadge = hasKey ? (
<Badge status="success" text="Active" />
const statusDot = hasKey ? (
<span className="inline-flex items-center gap-1.5 text-sm">
<span className="h-2 w-2 rounded-full bg-emerald-500" />
<span className="text-emerald-700 dark:text-emerald-400">Active</span>
</span>
) : (
<Badge status="warning" text="Needs Setup" />
<span className="inline-flex items-center gap-1.5 text-sm">
<span className="h-2 w-2 rounded-full bg-amber-500" />
<span className="text-amber-700 dark:text-amber-400">Needs Setup</span>
</span>
);
const copyToClipboard = (e: React.MouseEvent, text: string) => {
@ -37,61 +51,73 @@ const AgentCard: React.FC<AgentCardProps> = ({
return (
<Card
hoverable
className="h-full flex flex-col"
styles={{
body: { flex: 1, display: "flex", flexDirection: "column" },
}}
className="h-full flex flex-col cursor-pointer hover:shadow-md transition-shadow p-4"
onClick={() => onAgentClick(agent.agent_id)}
>
<div className="flex items-start justify-between gap-2 mb-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-medium text-gray-900 truncate">
<span className="font-medium text-foreground truncate">
{agent.agent_name}
</span>
<Tooltip title="Copy Agent ID">
<CopyOutlined
onClick={(e) => copyToClipboard(e, agent.agent_id)}
className="cursor-pointer text-gray-400 hover:text-blue-500 text-xs shrink-0"
/>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={(e) => copyToClipboard(e, agent.agent_id)}
className="cursor-pointer text-muted-foreground hover:text-primary shrink-0"
aria-label="Copy Agent ID"
>
<Copy className="h-3 w-3" />
</button>
</TooltipTrigger>
<TooltipContent>Copy Agent ID</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="mt-1">{statusBadge}</div>
<div className="mt-1">{statusDot}</div>
</div>
{isAdmin && onDeleteClick && (
<Tooltip title="Delete agent">
<Button
type="text"
size="small"
danger
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
onDeleteClick(agent.agent_id, agent.agent_name);
}}
className="shrink-0 -mr-1"
/>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="shrink-0 -mr-1 text-destructive hover:text-destructive hover:bg-destructive/10 h-7 w-7"
onClick={(e) => {
e.stopPropagation();
onDeleteClick(agent.agent_id, agent.agent_name);
}}
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>Delete agent</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
<p className="text-sm text-gray-600 line-clamp-2 flex-1 mb-3">
<p className="text-sm text-muted-foreground line-clamp-2 flex-1 mb-3">
{description}
</p>
{url && (
<p className="text-xs text-gray-500 truncate mb-2" title={url}>
<p className="text-xs text-muted-foreground truncate mb-2" title={url}>
{url}
</p>
)}
<div className="mt-auto pt-3 border-t border-gray-100 text-xs">
<div className="mt-auto pt-3 border-t border-border text-xs">
{hasKey ? (
<div className="flex items-center gap-1.5 text-gray-600">
<KeyOutlined />
<span>{keyInfo?.key_alias || keyInfo?.token_prefix || "Key assigned"}</span>
<div className="flex items-center gap-1.5 text-muted-foreground">
<Key className="h-3 w-3" />
<span>
{keyInfo?.key_alias || keyInfo?.token_prefix || "Key assigned"}
</span>
</div>
) : (
<div className="flex items-center gap-1.5 text-amber-600">
<WarningOutlined />
<div className="flex items-center gap-1.5 text-amber-600 dark:text-amber-400">
<AlertTriangle className="h-3 w-3" />
<span>No key assigned</span>
</div>
)}

View file

@ -1,5 +1,5 @@
import React from "react";
import { Skeleton } from "antd";
import { Skeleton } from "@/components/ui/skeleton";
import AgentCard from "./agent_card";
import { Agent, AgentKeyInfo } from "./types";
@ -28,7 +28,12 @@ const AgentCardGrid: React.FC<AgentCardGridProps> = ({
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[1, 2, 3].map((i) => (
<Skeleton key={i} active paragraph={{ rows: 3 }} />
<div key={i} className="border border-border rounded-lg p-4 space-y-3">
<Skeleton className="h-5 w-1/2" />
<Skeleton className="h-3 w-full" />
<Skeleton className="h-3 w-5/6" />
<Skeleton className="h-3 w-3/4" />
</div>
))}
</div>
);
@ -36,8 +41,8 @@ const AgentCardGrid: React.FC<AgentCardGridProps> = ({
if (!agentsList || agentsList.length === 0) {
return (
<div className="rounded-lg border border-gray-200 bg-gray-50/50 py-12 text-center">
<p className="text-gray-500">
<div className="rounded-lg border border-border bg-muted/50 py-12 text-center">
<p className="text-muted-foreground">
{isAdmin
? "No agents found. Create one to get started."
: "No agents found. Contact an admin to create agents."}

View file

@ -1,12 +1,14 @@
import React from "react";
import { Title } from "@tremor/react";
import { Descriptions } from "antd";
import { Agent } from "./types";
interface AgentCostViewProps {
agent: Agent;
}
/**
* Compact key-value panel for the cost-configuration fields on the agent
* detail page.
*/
const AgentCostView: React.FC<AgentCostViewProps> = ({ agent }) => {
const params = agent.litellm_params;
@ -18,29 +20,41 @@ const AgentCostView: React.FC<AgentCostViewProps> = ({ agent }) => {
return null;
}
const rows: Array<{ label: string; value: string }> = [];
if (params.cost_per_query !== undefined)
rows.push({
label: "Cost Per Query",
value: `$${params.cost_per_query}`,
});
if (params.input_cost_per_token !== undefined)
rows.push({
label: "Input Cost Per Token",
value: `$${params.input_cost_per_token}`,
});
if (params.output_cost_per_token !== undefined)
rows.push({
label: "Output Cost Per Token",
value: `$${params.output_cost_per_token}`,
});
return (
<div style={{ marginTop: 24 }}>
<Title>Cost Configuration</Title>
<Descriptions bordered column={1} style={{ marginTop: 16 }}>
{params.cost_per_query !== undefined && (
<Descriptions.Item label="Cost Per Query">
${params.cost_per_query}
</Descriptions.Item>
)}
{params.input_cost_per_token !== undefined && (
<Descriptions.Item label="Input Cost Per Token">
${params.input_cost_per_token}
</Descriptions.Item>
)}
{params.output_cost_per_token !== undefined && (
<Descriptions.Item label="Output Cost Per Token">
${params.output_cost_per_token}
</Descriptions.Item>
)}
</Descriptions>
<div className="mt-6">
<h3 className="text-lg font-semibold">Cost Configuration</h3>
<div className="mt-4 border border-border rounded-md overflow-hidden">
{rows.map((row, i) => (
<div
key={row.label}
className={`grid grid-cols-[minmax(200px,1fr)_2fr] ${
i !== 0 ? "border-t border-border" : ""
}`}
>
<div className="bg-muted px-4 py-2 font-medium">{row.label}</div>
<div className="px-4 py-2">{row.value}</div>
</div>
))}
</div>
</div>
);
};
export default AgentCostView;