evo-ai/frontend/app/agents/AgentCard.tsx

507 lines
18 KiB
TypeScript

/*
┌──────────────────────────────────────────────────────────────────────────────┐
│ @author: Davidson Gomes │
│ @file: /app/agents/AgentCard.tsx │
│ Developed by: Davidson Gomes │
│ Creation date: May 13, 2025 │
│ Contact: contato@evolution-api.com │
├──────────────────────────────────────────────────────────────────────────────┤
│ @copyright © Evolution API 2025. All rights reserved. │
│ Licensed under the Apache License, Version 2.0 │
│ │
│ You may not use this file except in compliance with the License. │
│ You may obtain a copy of the License at │
│ │
│ http://www.apache.org/licenses/LICENSE-2.0 │
│ │
│ Unless required by applicable law or agreed to in writing, software │
│ distributed under the License is distributed on an "AS IS" BASIS, │
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
│ See the License for the specific language governing permissions and │
│ limitations under the License. │
├──────────────────────────────────────────────────────────────────────────────┤
│ @important │
│ For any future changes to the code in this file, it is recommended to │
│ include, together with the modification, the information of the developer │
│ who changed it and the date of modification. │
└──────────────────────────────────────────────────────────────────────────────┘
*/
"use client";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Folder } from "@/services/agentService";
import { Agent, AgentType } from "@/types/agent";
import { MCPServer } from "@/types/mcpServer";
import {
ArrowRight,
Bot,
BookOpenCheck,
ChevronDown,
ChevronUp,
Code,
ExternalLink,
GitBranch,
MoveRight,
Pencil,
RefreshCw,
Settings,
Share2,
Trash2,
Workflow,
TextSelect,
Download,
FlaskConical,
} from "lucide-react";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
import { exportAsJson } from "@/lib/utils";
interface AgentCardProps {
agent: Agent;
onEdit: (agent: Agent) => void;
onDelete: (agent: Agent) => void;
onMove: (agent: Agent) => void;
onShare?: (agent: Agent) => void;
onWorkflow?: (agentId: string) => void;
availableMCPs?: MCPServer[];
getApiKeyNameById?: (id: string | undefined) => string | null;
getAgentNameById?: (id: string) => string;
folders?: Folder[];
agents: Agent[];
}
export function AgentCard({
agent,
onEdit,
onDelete,
onMove,
onShare,
onWorkflow,
availableMCPs = [],
getApiKeyNameById = () => null,
getAgentNameById = (id) => id,
folders = [],
agents,
}: AgentCardProps) {
const [expanded, setExpanded] = useState(false);
const router = useRouter();
const getAgentTypeInfo = (type: AgentType) => {
const types: Record<
string,
{
label: string;
icon: React.ElementType;
color: string;
bgColor: string;
badgeClass: string;
}
> = {
llm: {
label: "LLM Agent",
icon: Code,
color: "#00cc7d",
bgColor: "bg-green-500/10",
badgeClass:
"bg-green-900/30 text-green-400 border-green-600/30 hover:bg-green-900/40",
},
a2a: {
label: "A2A Agent",
icon: ExternalLink,
color: "#6366f1",
bgColor: "bg-indigo-500/10",
badgeClass:
"bg-indigo-900/30 text-indigo-400 border-indigo-600/30 hover:bg-indigo-900/40",
},
sequential: {
label: "Sequential Agent",
icon: ArrowRight,
color: "#f59e0b",
bgColor: "bg-yellow-500/10",
badgeClass:
"bg-yellow-900/30 text-yellow-400 border-yellow-600/30 hover:bg-yellow-900/40",
},
parallel: {
label: "Parallel Agent",
icon: GitBranch,
color: "#8b5cf6",
bgColor: "bg-purple-500/10",
badgeClass:
"bg-purple-900/30 text-purple-400 border-purple-600/30 hover:bg-purple-900/40",
},
loop: {
label: "Loop Agent",
icon: RefreshCw,
color: "#ec4899",
bgColor: "bg-pink-500/10",
badgeClass:
"bg-orange-900/30 text-orange-400 border-orange-600/30 hover:bg-orange-900/40",
},
workflow: {
label: "Workflow Agent",
icon: Workflow,
color: "#3b82f6",
bgColor: "bg-blue-500/10",
badgeClass:
"bg-blue-900/30 text-blue-400 border-blue-700/40 hover:bg-blue-900/40",
},
task: {
label: "Task Agent",
icon: BookOpenCheck,
color: "#ef4444",
bgColor: "bg-red-500/10",
badgeClass:
"bg-red-900/30 text-red-400 border-red-600/30 hover:bg-red-900/40",
},
};
return (
types[type] || {
label: type,
icon: Bot,
color: "#94a3b8",
bgColor: "bg-slate-500/10",
badgeClass:
"bg-slate-900/30 text-slate-400 border-slate-600/30 hover:bg-slate-900/40",
}
);
};
const getAgentTypeIcon = (type: AgentType) => {
const typeInfo = getAgentTypeInfo(type);
const IconComponent = typeInfo.icon;
return (
<IconComponent className="h-5 w-5" style={{ color: typeInfo.color }} />
);
};
const getAgentTypeName = (type: AgentType) => {
return getAgentTypeInfo(type).label;
};
const getAgentTypeBgColor = (type: AgentType) => {
return getAgentTypeInfo(type).bgColor;
};
const getAgentTypeBadgeClass = (type: AgentType) => {
return getAgentTypeInfo(type).badgeClass;
};
const getFolderNameById = (id: string) => {
const folder = folders?.find((f) => f.id === id);
return folder?.name || id;
};
const getTotalTools = () => {
if (agent.type === "llm" && agent.config?.mcp_servers) {
return agent.config.mcp_servers.reduce(
(total, mcp) => total + (mcp.tools?.length || 0),
0
);
}
return 0;
};
const getCreatedAtFormatted = () => {
return new Date(agent.created_at).toLocaleDateString();
};
// Function to export the agent as JSON
const handleExportAgent = () => {
try {
exportAsJson(
agent,
`agent-${agent.name
.replace(/\s+/g, "-")
.toLowerCase()}-${agent.id.substring(0, 8)}`,
true,
agents
);
} catch (error) {
console.error("Error exporting agent:", error);
}
};
// Function to test the A2A agent in the lab
const handleTestA2A = () => {
// Use the agent card URL as base for A2A tests
const agentUrl = agent.agent_card_url?.replace(
"/.well-known/agent.json",
""
);
// Use the API key directly from the agent config
const apiKey = agent.config?.api_key;
// Build the URL with parameters for the lab tests
const params = new URLSearchParams();
if (agentUrl) {
params.set("agent_url", agentUrl);
}
if (apiKey) {
params.set("api_key", apiKey);
}
// Redirect to the lab tests in the "lab" tab
const testUrl = `/documentation?${params.toString()}#lab`;
router.push(testUrl);
};
return (
<Card className="w-full overflow-hidden border border-zinc-800 shadow-lg bg-gradient-to-br from-zinc-800 to-zinc-900">
<div
className={cn(
"p-4 flex justify-between items-center border-b border-zinc-800",
getAgentTypeBgColor(agent.type)
)}
>
<div className="flex items-center gap-2">
{getAgentTypeIcon(agent.type)}
<h3 className="font-medium text-white">{agent.name}</h3>
</div>
<Badge
variant="outline"
className={cn("border", getAgentTypeBadgeClass(agent.type))}
>
{getAgentTypeName(agent.type)}
</Badge>
</div>
<CardContent className="p-0">
<div className="p-4 border-b border-zinc-800">
<p className="text-sm text-zinc-300">
{agent.description && agent.description.length > 100
? `${agent.description.substring(0, 100)}...`
: agent.description}
</p>
</div>
<div
className={cn(
"p-4 flex justify-between items-center",
getAgentTypeBgColor(agent.type),
"bg-opacity-20"
)}
>
<div className="flex items-center gap-2">
<span className="text-xs text-zinc-500">Model:</span>
<span className="text-xs font-medium text-zinc-300">
{agent.type === "llm" ? agent.model : "N/A"}
</span>
</div>
<Button
variant="ghost"
size="sm"
className={cn("p-0 h-auto", {
"text-green-400 hover:text-green-300": agent.type === "llm",
"text-indigo-400 hover:text-indigo-300": agent.type === "a2a",
"text-yellow-400 hover:text-yellow-300":
agent.type === "sequential",
"text-purple-400 hover:text-purple-300":
agent.type === "parallel",
"text-orange-400 hover:text-orange-300": agent.type === "loop",
"text-blue-400 hover:text-blue-300": agent.type === "workflow",
"text-red-400 hover:text-red-300": agent.type === "task",
"text-zinc-400 hover:text-white": ![
"llm",
"a2a",
"sequential",
"parallel",
"loop",
"workflow",
"task",
].includes(agent.type),
})}
onClick={() => setExpanded(!expanded)}
>
{expanded ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
<span className="ml-1 text-xs">{expanded ? "Less" : "More"}</span>
</Button>
</div>
{expanded && (
<div className="p-4 bg-zinc-950 text-xs space-y-3 animate-in fade-in-50 duration-200">
{agent.folder_id && (
<div className="flex justify-between items-center">
<span className="text-zinc-500">Folder:</span>
<Badge
variant="outline"
className={cn(
"h-5 px-2 bg-transparent",
getAgentTypeBadgeClass(agent.type)
)}
>
{getFolderNameById(agent.folder_id)}
</Badge>
</div>
)}
{agent.type === "llm" && agent.api_key_id && (
<div className="flex justify-between items-center">
<span className="text-zinc-500">API Key:</span>
<Badge
variant="outline"
className={cn(
"h-5 px-2 bg-transparent",
getAgentTypeBadgeClass(agent.type)
)}
>
{getApiKeyNameById(agent.api_key_id)}
</Badge>
</div>
)}
{getTotalTools() > 0 && (
<div className="flex justify-between items-center">
<span className="text-zinc-500">Tools:</span>
<span className="text-zinc-300">{getTotalTools()}</span>
</div>
)}
{agent.config?.sub_agents && agent.config.sub_agents.length > 0 && (
<div className="flex justify-between items-center">
<span className="text-zinc-500">Sub-agents:</span>
<span className="text-zinc-300">
{agent.config.sub_agents.length}
</span>
</div>
)}
{agent.type === "workflow" && agent.config?.workflow && (
<div className="flex justify-between items-center">
<span className="text-zinc-500">Elements:</span>
<span className="text-zinc-300">
{agent.config.workflow.nodes?.length || 0} nodes,{" "}
{agent.config.workflow.edges?.length || 0} connections
</span>
</div>
)}
<div className="flex justify-between items-center">
<span className="text-zinc-500">Created at:</span>
<span className="text-zinc-300">{getCreatedAtFormatted()}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-zinc-500">ID:</span>
<span className="text-zinc-300 text-[10px]">{agent.id}</span>
</div>
</div>
)}
<div className="flex border-t border-zinc-800">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="flex-1 rounded-none h-12 text-zinc-400 hover:text-white hover:bg-zinc-800 focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none"
>
<Settings className="h-4 w-4 mr-2" />
Configure
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="bg-zinc-900 border-zinc-700"
side="bottom"
align="end"
>
<DropdownMenuItem
className="text-white hover:bg-zinc-800 cursor-pointer"
onClick={handleTestA2A}
>
<FlaskConical className="h-4 w-4 mr-2 text-emerald-400" />
Test A2A
</DropdownMenuItem>
<DropdownMenuItem
className="text-white hover:bg-zinc-800 cursor-pointer"
onClick={() => onEdit(agent)}
>
<Pencil className="h-4 w-4 mr-2 text-emerald-400" />
Edit Agent
</DropdownMenuItem>
<DropdownMenuItem
className="text-white hover:bg-zinc-800 cursor-pointer"
onClick={() => onMove(agent)}
>
<MoveRight className="h-4 w-4 mr-2 text-yellow-400" />
Move Agent
</DropdownMenuItem>
{onWorkflow && agent.type === "workflow" && (
<DropdownMenuItem
className="text-white hover:bg-zinc-800 cursor-pointer"
onClick={() => onWorkflow(agent.id)}
>
<Workflow className="h-4 w-4 mr-2 text-blue-400" />
Open Workflow
</DropdownMenuItem>
)}
<DropdownMenuItem
className="text-white hover:bg-zinc-800 cursor-pointer"
onClick={handleExportAgent}
>
<Download className="h-4 w-4 mr-2 text-purple-400" />
Export as JSON
</DropdownMenuItem>
{onShare && (
<DropdownMenuItem
className="text-white hover:bg-zinc-800 cursor-pointer"
onClick={() => onShare(agent)}
>
<Share2 className="h-4 w-4 mr-2 text-blue-400" />
Share Agent
</DropdownMenuItem>
)}
<DropdownMenuItem
className="text-red-500 hover:bg-zinc-800 cursor-pointer"
onClick={() => onDelete(agent)}
>
<Trash2 className="h-4 w-4 mr-2" />
Delete Agent
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="w-px bg-zinc-800" />
<a
href={agent.agent_card_url}
target="_blank"
rel="noopener noreferrer"
className={cn(
"flex-1 flex items-center justify-center rounded-none h-12 hover:bg-zinc-800",
{
"text-green-400 hover:text-green-300": agent.type === "llm",
"text-indigo-400 hover:text-indigo-300": agent.type === "a2a",
"text-yellow-400 hover:text-yellow-300":
agent.type === "sequential",
"text-purple-400 hover:text-purple-300":
agent.type === "parallel",
"text-orange-400 hover:text-orange-300": agent.type === "loop",
"text-blue-400 hover:text-blue-300": agent.type === "workflow",
"text-red-400 hover:text-red-300": agent.type === "task",
}
)}
>
<ExternalLink className="h-4 w-4 mr-2" />
Agent Card
</a>
</div>
</CardContent>
</Card>
);
}