mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
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:
parent
08b1c0b897
commit
e00efbf810
4 changed files with 113 additions and 66 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue