Commit inicial - upload de todos os arquivos da pasta
This commit is contained in:
506
frontend/app/agents/AgentCard.tsx
Normal file
506
frontend/app/agents/AgentCard.tsx
Normal file
@@ -0,0 +1,506 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @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>
|
||||
);
|
||||
}
|
||||
130
frontend/app/agents/AgentList.tsx
Normal file
130
frontend/app/agents/AgentList.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/AgentList.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 { Agent } from "@/types/agent";
|
||||
import { MCPServer } from "@/types/mcpServer";
|
||||
import { AgentCard } from "./AgentCard";
|
||||
import { EmptyState } from "./EmptyState";
|
||||
import { ApiKey, Folder } from "@/services/agentService";
|
||||
|
||||
interface AgentListProps {
|
||||
agents: Agent[];
|
||||
isLoading: boolean;
|
||||
searchTerm: string;
|
||||
selectedFolderId: string | null;
|
||||
availableMCPs: MCPServer[];
|
||||
getApiKeyNameById: (id: string | undefined) => string | null;
|
||||
getAgentNameById: (id: string) => string;
|
||||
onEdit: (agent: Agent) => void;
|
||||
onDelete: (agent: Agent) => void;
|
||||
onMove: (agent: Agent) => void;
|
||||
onShare?: (agent: Agent) => void;
|
||||
onWorkflow?: (agentId: string) => void;
|
||||
onClearSearch?: () => void;
|
||||
onCreateAgent?: () => void;
|
||||
apiKeys: ApiKey[];
|
||||
folders: Folder[];
|
||||
}
|
||||
|
||||
export function AgentList({
|
||||
agents,
|
||||
isLoading,
|
||||
searchTerm,
|
||||
selectedFolderId,
|
||||
availableMCPs,
|
||||
getApiKeyNameById,
|
||||
getAgentNameById,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onMove,
|
||||
onShare,
|
||||
onWorkflow,
|
||||
onClearSearch,
|
||||
onCreateAgent,
|
||||
apiKeys,
|
||||
folders,
|
||||
}: AgentListProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[60vh]">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-emerald-400"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (agents.length === 0) {
|
||||
if (searchTerm) {
|
||||
return (
|
||||
<EmptyState
|
||||
type="search-no-results"
|
||||
searchTerm={searchTerm}
|
||||
onAction={onClearSearch}
|
||||
/>
|
||||
);
|
||||
} else if (selectedFolderId) {
|
||||
return (
|
||||
<EmptyState
|
||||
type="empty-folder"
|
||||
onAction={onCreateAgent}
|
||||
actionLabel="Create Agent"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<EmptyState
|
||||
type="no-agents"
|
||||
onAction={onCreateAgent}
|
||||
actionLabel="Create Agent"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{agents.map((agent) => (
|
||||
<AgentCard
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onMove={onMove}
|
||||
onShare={onShare}
|
||||
onWorkflow={onWorkflow}
|
||||
availableMCPs={availableMCPs}
|
||||
getApiKeyNameById={getApiKeyNameById}
|
||||
getAgentNameById={getAgentNameById}
|
||||
folders={folders}
|
||||
agents={agents}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
186
frontend/app/agents/AgentSidebar.tsx
Normal file
186
frontend/app/agents/AgentSidebar.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/AgentSidebar.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 { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Folder,
|
||||
FolderPlus,
|
||||
Home,
|
||||
X,
|
||||
CircleEllipsis,
|
||||
Edit,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
|
||||
interface AgentFolder {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface AgentSidebarProps {
|
||||
visible: boolean;
|
||||
folders: AgentFolder[];
|
||||
selectedFolderId: string | null;
|
||||
onSelectFolder: (id: string | null) => void;
|
||||
onAddFolder: () => void;
|
||||
onEditFolder: (folder: AgentFolder) => void;
|
||||
onDeleteFolder: (folder: AgentFolder) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function AgentSidebar({
|
||||
visible,
|
||||
folders,
|
||||
selectedFolderId,
|
||||
onSelectFolder,
|
||||
onAddFolder,
|
||||
onEditFolder,
|
||||
onDeleteFolder,
|
||||
onClose,
|
||||
}: AgentSidebarProps) {
|
||||
return (
|
||||
<>
|
||||
{visible && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute right-4 top-4 z-40 bg-[#222] p-2 rounded-md text-emerald-400 hover:bg-[#333] hover:text-emerald-400 shadow-md transition-all"
|
||||
aria-label="Hide folders"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`fixed top-0 z-30 h-full w-64 bg-[#1a1a1a] p-4 shadow-xl transition-all duration-300 ease-in-out ${
|
||||
visible ? "left-64 translate-x-0" : "left-0 -translate-x-full pointer-events-none"
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-semibold text-white flex items-center">
|
||||
<Folder className="h-5 w-5 mr-2 text-emerald-400" />
|
||||
Folders
|
||||
</h2>
|
||||
<div className="flex space-x-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0 text-neutral-400 hover:text-emerald-400 hover:bg-[#222]"
|
||||
onClick={onAddFolder}
|
||||
>
|
||||
<FolderPlus className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0 text-neutral-400 hover:text-emerald-400 hover:bg-[#222]"
|
||||
onClick={onClose}
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<button
|
||||
className={`w-full text-left px-3 py-2 rounded-md flex items-center ${
|
||||
selectedFolderId === null
|
||||
? "bg-[#333] text-emerald-400"
|
||||
: "text-neutral-300 hover:bg-[#222] hover:text-white"
|
||||
}`}
|
||||
onClick={() => onSelectFolder(null)}
|
||||
>
|
||||
<Home className="h-4 w-4 mr-2" />
|
||||
<span>All agents</span>
|
||||
</button>
|
||||
|
||||
{folders.map((folder) => (
|
||||
<div key={folder.id} className="flex items-center group">
|
||||
<button
|
||||
className={`flex-1 text-left px-3 py-2 rounded-md flex items-center ${
|
||||
selectedFolderId === folder.id
|
||||
? "bg-[#333] text-emerald-400"
|
||||
: "text-neutral-300 hover:bg-[#222] hover:text-white"
|
||||
}`}
|
||||
onClick={() => onSelectFolder(folder.id)}
|
||||
>
|
||||
<Folder className="h-4 w-4 mr-2" />
|
||||
<span className="truncate">{folder.name}</span>
|
||||
</button>
|
||||
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 p-0 text-neutral-400 hover:text-white hover:bg-[#222]"
|
||||
>
|
||||
<CircleEllipsis className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="bg-[#222] border-[#333] text-white"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer hover:bg-[#333] focus:bg-[#333]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEditFolder(folder);
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer text-red-500 hover:bg-[#333] hover:text-red-400 focus:bg-[#333]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteFolder(folder);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
96
frontend/app/agents/AgentTypeSelector.tsx
Normal file
96
frontend/app/agents/AgentTypeSelector.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/AgentTypeSelector.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 { AgentType } from "@/types/agent";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Code,
|
||||
ExternalLink,
|
||||
GitBranch,
|
||||
RefreshCw,
|
||||
Workflow,
|
||||
Users,
|
||||
BookOpenCheck,
|
||||
} from "lucide-react";
|
||||
|
||||
interface AgentTypeSelectorProps {
|
||||
value: AgentType;
|
||||
onValueChange: (value: AgentType) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const agentTypes = [
|
||||
{ value: "llm", label: "LLM Agent", icon: Code },
|
||||
{ value: "a2a", label: "A2A Agent", icon: ExternalLink },
|
||||
{ value: "sequential", label: "Sequential Agent", icon: Workflow },
|
||||
{ value: "parallel", label: "Parallel Agent", icon: GitBranch },
|
||||
{ value: "loop", label: "Loop Agent", icon: RefreshCw },
|
||||
{ value: "workflow", label: "Workflow Agent", icon: Workflow },
|
||||
{ value: "task", label: "Task Agent", icon: BookOpenCheck },
|
||||
];
|
||||
|
||||
export function AgentTypeSelector({
|
||||
value,
|
||||
onValueChange,
|
||||
className = "",
|
||||
}: AgentTypeSelectorProps) {
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(value: AgentType) => onValueChange(value)}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={`bg-[#222] border-[#444] text-white ${className}`}
|
||||
>
|
||||
<SelectValue placeholder="Select type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#222] border-[#444] text-white">
|
||||
{agentTypes.map((type) => (
|
||||
<SelectItem
|
||||
key={type.value}
|
||||
value={type.value}
|
||||
className="data-[selected]:bg-[#333] data-[highlighted]:bg-[#333] text-white focus:!text-white hover:text-emerald-400 data-[selected]:!text-emerald-400"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<type.icon className="h-4 w-4 text-emerald-400" />
|
||||
{type.label}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
107
frontend/app/agents/EmptyState.tsx
Normal file
107
frontend/app/agents/EmptyState.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/EmptyState.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 { Button } from "@/components/ui/button";
|
||||
import { Folder, Plus, Search, Server } from "lucide-react";
|
||||
|
||||
interface EmptyStateProps {
|
||||
type: "no-agents" | "empty-folder" | "search-no-results";
|
||||
searchTerm?: string;
|
||||
onAction?: () => void;
|
||||
actionLabel?: string;
|
||||
}
|
||||
|
||||
export function EmptyState({
|
||||
type,
|
||||
searchTerm = "",
|
||||
onAction,
|
||||
actionLabel = "Create Agent",
|
||||
}: EmptyStateProps) {
|
||||
const getIcon = () => {
|
||||
switch (type) {
|
||||
case "empty-folder":
|
||||
return <Folder className="h-16 w-16 text-emerald-400" />;
|
||||
case "search-no-results":
|
||||
return <Search className="h-16 w-16 text-emerald-400" />;
|
||||
case "no-agents":
|
||||
default:
|
||||
return <Server className="h-16 w-16 text-emerald-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getTitle = () => {
|
||||
switch (type) {
|
||||
case "empty-folder":
|
||||
return "Empty folder";
|
||||
case "search-no-results":
|
||||
return "No agents found";
|
||||
case "no-agents":
|
||||
default:
|
||||
return "No agents found";
|
||||
}
|
||||
};
|
||||
|
||||
const getMessage = () => {
|
||||
switch (type) {
|
||||
case "empty-folder":
|
||||
return "This folder is empty. Add agents or create a new one.";
|
||||
case "search-no-results":
|
||||
return `We couldn't find any agents that match your search: "${searchTerm}"`;
|
||||
case "no-agents":
|
||||
default:
|
||||
return "You don't have any agents configured. Create your first agent to start!";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-[60vh] text-center">
|
||||
<div className="mb-6 p-8 rounded-full bg-[#1a1a1a] border border-[#333]">
|
||||
{getIcon()}
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold text-white mb-3">{getTitle()}</h2>
|
||||
<p className="text-neutral-300 mb-6 max-w-md">{getMessage()}</p>
|
||||
{onAction && (
|
||||
<Button
|
||||
onClick={onAction}
|
||||
className={
|
||||
type === "search-no-results"
|
||||
? "bg-[#222] text-white hover:bg-[#333]"
|
||||
: "bg-emerald-400 text-black hover:bg-[#00cc7d] px-6 py-2 hover:shadow-[0_0_15px_rgba(0,255,157,0.2)]"
|
||||
}
|
||||
>
|
||||
{type === "search-no-results" ? null : (
|
||||
<Plus className="mr-2 h-5 w-5" />
|
||||
)}
|
||||
{type === "search-no-results" ? "Clear search" : actionLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
153
frontend/app/agents/SearchInput.tsx
Normal file
153
frontend/app/agents/SearchInput.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/SearchInput.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 { useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Search, X, Filter } from "lucide-react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
|
||||
interface SearchInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
selectedAgentType?: string | null;
|
||||
onAgentTypeChange?: (type: string | null) => void;
|
||||
agentTypes?: string[];
|
||||
}
|
||||
|
||||
// Using "all" as a special value to represent no filter
|
||||
const ANY_TYPE_VALUE = "all";
|
||||
|
||||
export function SearchInput({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "Search agents...",
|
||||
className = "",
|
||||
selectedAgentType = null,
|
||||
onAgentTypeChange,
|
||||
agentTypes = [],
|
||||
}: SearchInputProps) {
|
||||
const [isFilterOpen, setIsFilterOpen] = useState(false);
|
||||
|
||||
const handleTypeChange = (value: string) => {
|
||||
if (onAgentTypeChange) {
|
||||
onAgentTypeChange(value === ANY_TYPE_VALUE ? null : value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`relative flex items-center gap-2 ${className}`}>
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-neutral-400" />
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
autoComplete="off"
|
||||
className="pl-10 w-full bg-[#222] border-[#444] text-white focus:border-emerald-400 focus:ring-emerald-400/10"
|
||||
/>
|
||||
{value && (
|
||||
<button
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white"
|
||||
onClick={() => onChange("")}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{agentTypes.length > 0 && onAgentTypeChange && (
|
||||
<Popover open={isFilterOpen} onOpenChange={setIsFilterOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={`px-3 py-2 bg-[#222] border-[#444] hover:bg-[#333] ${
|
||||
selectedAgentType ? "text-emerald-400" : "text-neutral-400"
|
||||
}`}
|
||||
>
|
||||
<Filter className="h-4 w-4 mr-1" />
|
||||
{selectedAgentType ? "Filtered" : "Filter"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-48 p-2 bg-[#222] border-[#444]">
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs font-medium text-neutral-300">
|
||||
Filter by type
|
||||
</div>
|
||||
<Select
|
||||
value={selectedAgentType ? selectedAgentType : ANY_TYPE_VALUE}
|
||||
onValueChange={handleTypeChange}
|
||||
>
|
||||
<SelectTrigger className="bg-[#333] border-[#444] text-white">
|
||||
<SelectValue placeholder="Any type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#333] border-[#444] text-white">
|
||||
<SelectItem value={ANY_TYPE_VALUE}>Any type</SelectItem>
|
||||
{agentTypes.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{type}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{selectedAgentType && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full text-xs text-neutral-300 hover:text-white"
|
||||
onClick={() => {
|
||||
onAgentTypeChange(null);
|
||||
setIsFilterOpen(false);
|
||||
}}
|
||||
>
|
||||
Clear filter
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
73
frontend/app/agents/config/A2AAgentConfig.tsx
Normal file
73
frontend/app/agents/config/A2AAgentConfig.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/config/A2AAgentConfig.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 { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
interface A2AAgentConfigProps {
|
||||
values: {
|
||||
agent_card_url?: string;
|
||||
};
|
||||
onChange: (values: any) => void;
|
||||
}
|
||||
|
||||
export function A2AAgentConfig({ values, onChange }: A2AAgentConfigProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="agent_card_url" className="text-right text-neutral-300">
|
||||
Agent Card URL
|
||||
</Label>
|
||||
<Input
|
||||
id="agent_card_url"
|
||||
value={values.agent_card_url || ""}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...values,
|
||||
agent_card_url: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="https://example.com/.well-known/agent-card.json"
|
||||
className="col-span-3 bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="pl-[25%] text-sm text-neutral-400">
|
||||
<p>
|
||||
Provide the full URL for the JSON file of the Agent Card that describes
|
||||
this agent.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Agent Cards contain metadata, capabilities descriptions and supported
|
||||
protocols.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
367
frontend/app/agents/config/LLMAgentConfig.tsx
Normal file
367
frontend/app/agents/config/LLMAgentConfig.tsx
Normal file
@@ -0,0 +1,367 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/config/LLMAgentConfig.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 { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ApiKey } from "@/services/agentService";
|
||||
import { Plus, Maximize2, Save } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
interface ModelOption {
|
||||
value: string;
|
||||
label: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
interface LLMAgentConfigProps {
|
||||
apiKeys: ApiKey[];
|
||||
availableModels: ModelOption[];
|
||||
values: {
|
||||
model?: string;
|
||||
api_key_id?: string;
|
||||
instruction?: string;
|
||||
role?: string;
|
||||
goal?: string;
|
||||
};
|
||||
onChange: (values: any) => void;
|
||||
onOpenApiKeysDialog: () => void;
|
||||
}
|
||||
|
||||
export function LLMAgentConfig({
|
||||
apiKeys,
|
||||
availableModels,
|
||||
values,
|
||||
onChange,
|
||||
onOpenApiKeysDialog,
|
||||
}: LLMAgentConfigProps) {
|
||||
const [instructionText, setInstructionText] = useState(values.instruction || "");
|
||||
const [isInstructionModalOpen, setIsInstructionModalOpen] = useState(false);
|
||||
const [expandedInstructionText, setExpandedInstructionText] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setInstructionText(values.instruction || "");
|
||||
}, [values.instruction]);
|
||||
|
||||
const handleInstructionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newValue = e.target.value;
|
||||
setInstructionText(newValue);
|
||||
|
||||
onChange({
|
||||
...values,
|
||||
instruction: newValue,
|
||||
});
|
||||
};
|
||||
|
||||
const handleExpandInstruction = () => {
|
||||
setExpandedInstructionText(instructionText);
|
||||
setIsInstructionModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveExpandedInstruction = () => {
|
||||
setInstructionText(expandedInstructionText);
|
||||
onChange({
|
||||
...values,
|
||||
instruction: expandedInstructionText,
|
||||
});
|
||||
setIsInstructionModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="role" className="text-right text-neutral-300">
|
||||
Role
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
<Input
|
||||
id="role"
|
||||
value={values.role || ""}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...values,
|
||||
role: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="Ex: Research Assistant, Customer Support, etc."
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
<div className="mt-1 text-xs text-neutral-400">
|
||||
<span className="inline-block h-3 w-3 mr-1">ℹ️</span>
|
||||
<span>Define the role or persona that the agent will assume</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="goal" className="text-right text-neutral-300">
|
||||
Goal
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
<Input
|
||||
id="goal"
|
||||
value={values.goal || ""}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...values,
|
||||
goal: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="Ex: Find and organize information, Assist customers with inquiries, etc."
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
<div className="mt-1 text-xs text-neutral-400">
|
||||
<span className="inline-block h-3 w-3 mr-1">ℹ️</span>
|
||||
<span>Define the main objective or purpose of this agent</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="api_key" className="text-right text-neutral-300">
|
||||
API Key
|
||||
</Label>
|
||||
<div className="col-span-3 space-y-4">
|
||||
<div className="flex items-center">
|
||||
<Select
|
||||
value={values.api_key_id || ""}
|
||||
onValueChange={(value) =>
|
||||
onChange({
|
||||
...values,
|
||||
api_key_id: value,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="flex-1 bg-[#222] border-[#444] text-white">
|
||||
<SelectValue placeholder="Select an API key" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#222] border-[#444] text-white">
|
||||
{apiKeys.length > 0 ? (
|
||||
apiKeys
|
||||
.filter((key) => key.is_active !== false)
|
||||
.map((key) => (
|
||||
<SelectItem
|
||||
key={key.id}
|
||||
value={key.id}
|
||||
className="data-[selected]:bg-[#333] data-[highlighted]:bg-[#333] !text-white focus:!text-white hover:text-emerald-400 data-[selected]:!text-emerald-400"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span>{key.name}</span>
|
||||
<Badge className="ml-2 bg-[#333] text-emerald-400 text-xs">
|
||||
{key.provider}
|
||||
</Badge>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<div className="text-neutral-500 px-2 py-1.5 pl-8">
|
||||
No API keys available
|
||||
</div>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onOpenApiKeysDialog}
|
||||
className="ml-2 bg-[#222] text-emerald-400 hover:bg-[#333]"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{apiKeys.length === 0 && (
|
||||
<div className="flex items-center text-xs text-neutral-400">
|
||||
<span className="inline-block h-3 w-3 mr-1 text-neutral-400">i</span>
|
||||
<span>
|
||||
You need to{" "}
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={onOpenApiKeysDialog}
|
||||
className="h-auto p-0 text-xs text-emerald-400"
|
||||
>
|
||||
register API keys
|
||||
</Button>{" "}
|
||||
before creating an agent.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="model" className="text-right text-neutral-300">
|
||||
Model
|
||||
</Label>
|
||||
<Select
|
||||
value={values.model}
|
||||
onValueChange={(value) =>
|
||||
onChange({
|
||||
...values,
|
||||
model: value,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="col-span-3 bg-[#222] border-[#444] text-white">
|
||||
<SelectValue placeholder="Select the model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#222] border-[#444] text-white p-0">
|
||||
<div className="sticky top-0 z-10 p-2 bg-[#222] border-b border-[#444]">
|
||||
<Input
|
||||
placeholder="Search models..."
|
||||
className="bg-[#333] border-[#444] text-white h-8"
|
||||
onChange={(e) => {
|
||||
const searchQuery = e.target.value.toLowerCase();
|
||||
const items = document.querySelectorAll('[data-model-item="true"]');
|
||||
items.forEach((item) => {
|
||||
const text = item.textContent?.toLowerCase() || '';
|
||||
if (text.includes(searchQuery)) {
|
||||
(item as HTMLElement).style.display = 'flex';
|
||||
} else {
|
||||
(item as HTMLElement).style.display = 'none';
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-[200px] overflow-y-auto py-1">
|
||||
{availableModels
|
||||
.filter((model) => {
|
||||
if (!values.api_key_id) return true;
|
||||
|
||||
const selectedKey = apiKeys.find(
|
||||
(key) => key.id === values.api_key_id
|
||||
);
|
||||
|
||||
if (!selectedKey) return true;
|
||||
|
||||
return model.provider === selectedKey.provider;
|
||||
})
|
||||
.map((model) => (
|
||||
<SelectItem
|
||||
key={model.value}
|
||||
value={model.value}
|
||||
className="data-[selected]:bg-[#333] data-[highlighted]:bg-[#333] !text-white focus:!text-white hover:text-emerald-400 data-[selected]:!text-emerald-400"
|
||||
data-model-item="true"
|
||||
>
|
||||
{model.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="instruction" className="text-right text-neutral-300">
|
||||
Instructions
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
id="instruction"
|
||||
value={instructionText}
|
||||
onChange={handleInstructionChange}
|
||||
className="w-full bg-[#222] border-[#444] text-white pr-10"
|
||||
rows={4}
|
||||
onClick={handleExpandInstruction}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-3 right-5 text-neutral-400 hover:text-emerald-400 focus:outline-none"
|
||||
onClick={handleExpandInstruction}
|
||||
>
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-neutral-400">
|
||||
<span className="inline-block h-3 w-3 mr-1">ℹ️</span>
|
||||
<span>
|
||||
Characters like {"{"} and {"}"} or {"{{"} and {"}}"} are automatically escaped to avoid errors in Python.
|
||||
<span className="ml-2 text-emerald-400">Click to expand editor.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Instruction Modal */}
|
||||
<Dialog open={isInstructionModalOpen} onOpenChange={setIsInstructionModalOpen}>
|
||||
<DialogContent className="sm:max-w-[1200px] max-h-[90vh] bg-[#1a1a1a] border-[#333] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Agent Instructions</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-hidden flex flex-col min-h-[60vh]">
|
||||
<Textarea
|
||||
value={expandedInstructionText}
|
||||
onChange={(e) => setExpandedInstructionText(e.target.value)}
|
||||
className="flex-1 min-h-full bg-[#222] border-[#444] text-white p-4 focus:border-emerald-400 focus:ring-emerald-400 focus:ring-opacity-50 resize-none"
|
||||
placeholder="Enter detailed instructions for the agent..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsInstructionModalOpen(false)}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveExpandedInstruction}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Save Instructions
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
133
frontend/app/agents/config/LoopAgentConfig copy.tsx
Normal file
133
frontend/app/agents/config/LoopAgentConfig copy.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/config/LoopAgentConfig.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 { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
|
||||
interface LoopAgentConfigProps {
|
||||
values: {
|
||||
config?: {
|
||||
sub_agents?: string[];
|
||||
max_iterations?: number;
|
||||
};
|
||||
};
|
||||
onChange: (values: any) => void;
|
||||
agents: Agent[];
|
||||
getAgentNameById: (id: string) => string;
|
||||
}
|
||||
|
||||
export function LoopAgentConfig({
|
||||
values,
|
||||
onChange,
|
||||
agents,
|
||||
getAgentNameById,
|
||||
}: LoopAgentConfigProps) {
|
||||
const handleMaxIterationsChange = (value: string) => {
|
||||
const maxIterations = parseInt(value);
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...values.config,
|
||||
max_iterations: isNaN(maxIterations) ? undefined : maxIterations,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="max_iterations" className="text-right text-neutral-300">
|
||||
Max. Iterations
|
||||
</Label>
|
||||
<Input
|
||||
id="max_iterations"
|
||||
type="number"
|
||||
min={1}
|
||||
max={100}
|
||||
value={values.config?.max_iterations || ""}
|
||||
onChange={(e) => handleMaxIterationsChange(e.target.value)}
|
||||
className="col-span-3 bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||
<h3 className="text-sm font-medium text-white mb-4">
|
||||
Execution Order of Agents
|
||||
</h3>
|
||||
|
||||
{values.config?.sub_agents && values.config.sub_agents.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{values.config.sub_agents.map((agentId, index) => (
|
||||
<div
|
||||
key={agentId}
|
||||
className="flex items-center space-x-2 bg-[#2a2a2a] p-3 rounded-md"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-white">
|
||||
{getAgentNameById(agentId)}
|
||||
</div>
|
||||
<div className="text-sm text-neutral-400">
|
||||
Executed on{" "}
|
||||
<Badge className="bg-[#333] text-emerald-400 border-none">
|
||||
Position {index + 1}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-6 text-neutral-400">
|
||||
Add agents in the "Sub-Agents" tab to define the execution order
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3 text-sm text-neutral-400">
|
||||
<p>
|
||||
The agents will be executed sequentially in the order listed above.
|
||||
The output of each agent will be provided as input to the next
|
||||
agent in the sequence.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
125
frontend/app/agents/config/ParallelAgentConfig.tsx
Normal file
125
frontend/app/agents/config/ParallelAgentConfig.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/config/ParallelAgentConfig.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 { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { GitBranch } from "lucide-react";
|
||||
|
||||
interface ParallelAgentConfigProps {
|
||||
values: {
|
||||
config?: {
|
||||
sub_agents?: string[];
|
||||
aggregation_method?: string;
|
||||
timeout_seconds?: number;
|
||||
custom_mcp_servers?: any[];
|
||||
wait_for_all?: boolean;
|
||||
};
|
||||
};
|
||||
onChange: (values: any) => void;
|
||||
agents: Agent[];
|
||||
getAgentNameById: (id: string) => string;
|
||||
}
|
||||
|
||||
export function ParallelAgentConfig({
|
||||
values,
|
||||
onChange,
|
||||
agents,
|
||||
getAgentNameById,
|
||||
}: ParallelAgentConfigProps) {
|
||||
const aggregationMethods = [
|
||||
{ value: "merge", label: "Merge all responses" },
|
||||
{ value: "first", label: "Use only the first response" },
|
||||
{ value: "last", label: "Use only the last response" },
|
||||
{ value: "custom", label: "Custom aggregation" },
|
||||
];
|
||||
|
||||
const handleTimeoutChange = (value: string) => {
|
||||
const timeout = parseInt(value);
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...values.config,
|
||||
timeout_seconds: isNaN(timeout) ? undefined : timeout,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||
<h3 className="text-sm font-medium text-white mb-4">
|
||||
Agents in Parallel
|
||||
</h3>
|
||||
|
||||
{values.config?.sub_agents && values.config.sub_agents.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{values.config.sub_agents.map((agentId) => (
|
||||
<div
|
||||
key={agentId}
|
||||
className="flex items-center space-x-2 bg-[#2a2a2a] p-3 rounded-md"
|
||||
>
|
||||
<GitBranch className="h-5 w-5 text-emerald-400" />
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-white truncate">
|
||||
{getAgentNameById(agentId)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-6 text-neutral-400">
|
||||
Add agents in the "Sub-Agents" tab to execute in parallel
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3 text-sm text-neutral-400">
|
||||
<p>
|
||||
All listed agents will be executed simultaneously with the same
|
||||
input. The responses will be aggregated according to the selected
|
||||
method.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
120
frontend/app/agents/config/SequentialAgentConfig.tsx
Normal file
120
frontend/app/agents/config/SequentialAgentConfig.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/config/SequentialAgentConfig.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 { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
|
||||
interface SequentialAgentConfigProps {
|
||||
values: {
|
||||
config?: {
|
||||
sub_agents?: string[];
|
||||
max_iterations?: number;
|
||||
custom_mcp_servers?: any[];
|
||||
};
|
||||
};
|
||||
onChange: (values: any) => void;
|
||||
agents: Agent[];
|
||||
getAgentNameById: (id: string) => string;
|
||||
}
|
||||
|
||||
export function SequentialAgentConfig({
|
||||
values,
|
||||
onChange,
|
||||
agents,
|
||||
getAgentNameById,
|
||||
}: SequentialAgentConfigProps) {
|
||||
const handleMaxIterationsChange = (value: string) => {
|
||||
const maxIterations = parseInt(value);
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...values.config,
|
||||
max_iterations: isNaN(maxIterations) ? undefined : maxIterations,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||
<h3 className="text-sm font-medium text-white mb-4">
|
||||
Execution Order of Agents
|
||||
</h3>
|
||||
|
||||
{values.config?.sub_agents && values.config.sub_agents.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{values.config.sub_agents.map((agentId, index) => (
|
||||
<div
|
||||
key={agentId}
|
||||
className="flex items-center space-x-2 bg-[#2a2a2a] p-3 rounded-md"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-white">
|
||||
{getAgentNameById(agentId)}
|
||||
</div>
|
||||
<div className="text-sm text-neutral-400">
|
||||
Executed on{" "}
|
||||
<Badge className="bg-[#333] text-emerald-400 border-none">
|
||||
Position {index + 1}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-6 text-neutral-400">
|
||||
Add agents in the "Sub-Agents" tab to define the execution order
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3 text-sm text-neutral-400">
|
||||
<p>
|
||||
The agents will be executed sequentially in the order listed above.
|
||||
The output of each agent will be provided as input to the next
|
||||
agent in the sequence.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
801
frontend/app/agents/config/TaskAgentConfig.tsx
Normal file
801
frontend/app/agents/config/TaskAgentConfig.tsx
Normal file
@@ -0,0 +1,801 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/config/TaskAgentConfig.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 { Agent, TaskConfig } from "@/types/agent";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Maximize2,
|
||||
Save,
|
||||
X,
|
||||
ArrowDown,
|
||||
List,
|
||||
Search,
|
||||
Edit,
|
||||
PenTool,
|
||||
} from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
|
||||
interface TaskAgentConfigProps {
|
||||
values: Partial<Agent>;
|
||||
onChange: (values: Partial<Agent>) => void;
|
||||
agents: Agent[];
|
||||
getAgentNameById: (id: string) => string;
|
||||
singleTask?: boolean;
|
||||
}
|
||||
|
||||
const getAgentTypeLabel = (type: string): string => {
|
||||
const typeMap: Record<string, string> = {
|
||||
llm: "LLM",
|
||||
a2a: "A2A",
|
||||
sequential: "Sequential",
|
||||
parallel: "Parallel",
|
||||
loop: "Loop",
|
||||
workflow: "Workflow",
|
||||
task: "Task",
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
};
|
||||
|
||||
const getAgentTypeColor = (type: string): string => {
|
||||
const colorMap: Record<string, string> = {
|
||||
llm: "bg-blue-800 text-white",
|
||||
a2a: "bg-purple-800 text-white",
|
||||
sequential: "bg-orange-800 text-white",
|
||||
parallel: "bg-green-800 text-white",
|
||||
loop: "bg-pink-800 text-white",
|
||||
workflow: "bg-yellow-800 text-black",
|
||||
task: "bg-green-800 text-white",
|
||||
};
|
||||
return colorMap[type] || "bg-neutral-800 text-white";
|
||||
};
|
||||
|
||||
export function TaskAgentConfig({
|
||||
values,
|
||||
onChange,
|
||||
agents,
|
||||
getAgentNameById,
|
||||
singleTask = false,
|
||||
}: TaskAgentConfigProps) {
|
||||
const [newTask, setNewTask] = useState<TaskConfig>({
|
||||
agent_id: "",
|
||||
description: "",
|
||||
expected_output: "",
|
||||
enabled_tools: [],
|
||||
});
|
||||
|
||||
const [taskAgentSearchQuery, setTaskAgentSearchQuery] = useState<string>("");
|
||||
const [filteredTaskAgents, setFilteredTaskAgents] = useState<Agent[]>([]);
|
||||
const [isDescriptionModalOpen, setIsDescriptionModalOpen] = useState(false);
|
||||
const [expandedDescription, setExpandedDescription] = useState("");
|
||||
const [editingTaskIndex, setEditingTaskIndex] = useState<number | null>(null);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const [toolSearchQuery, setToolSearchQuery] = useState<string>("");
|
||||
const [filteredTools, setFilteredTools] = useState<{id: string, name: string}[]>([]);
|
||||
const [isToolsModalOpen, setIsToolsModalOpen] = useState(false);
|
||||
const [tempSelectedTools, setTempSelectedTools] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isToolsModalOpen) {
|
||||
if (isEditing && editingTaskIndex !== null && values.config?.tasks) {
|
||||
setTempSelectedTools(
|
||||
values.config.tasks[editingTaskIndex]?.enabled_tools || []
|
||||
);
|
||||
} else {
|
||||
setTempSelectedTools([...newTask.enabled_tools || []]);
|
||||
}
|
||||
}
|
||||
}, [isToolsModalOpen]);
|
||||
|
||||
const getAvailableTaskAgents = (currentTaskAgentId?: string) =>
|
||||
agents.filter(
|
||||
(agent) =>
|
||||
agent.id !== values.id &&
|
||||
(!values.config?.tasks?.some((task) => task.agent_id === agent.id) ||
|
||||
agent.id === currentTaskAgentId)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const currentTaskAgentId =
|
||||
isEditing && editingTaskIndex !== null && values.config?.tasks
|
||||
? values.config.tasks[editingTaskIndex].agent_id
|
||||
: undefined;
|
||||
|
||||
const availableAgents = getAvailableTaskAgents(currentTaskAgentId);
|
||||
|
||||
if (taskAgentSearchQuery.trim() === "") {
|
||||
setFilteredTaskAgents(availableAgents);
|
||||
} else {
|
||||
const query = taskAgentSearchQuery.toLowerCase();
|
||||
setFilteredTaskAgents(
|
||||
availableAgents.filter(
|
||||
(agent) =>
|
||||
agent.name.toLowerCase().includes(query) ||
|
||||
(agent.description?.toLowerCase() || "").includes(query)
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [
|
||||
taskAgentSearchQuery,
|
||||
agents,
|
||||
values.config?.tasks,
|
||||
isEditing,
|
||||
editingTaskIndex,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset editing state when values change externally
|
||||
if (!isEditing) {
|
||||
const currentTaskAgentId =
|
||||
editingTaskIndex !== null && values.config?.tasks
|
||||
? values.config.tasks[editingTaskIndex]?.agent_id
|
||||
: undefined;
|
||||
setFilteredTaskAgents(getAvailableTaskAgents(currentTaskAgentId));
|
||||
}
|
||||
}, [agents, values.config?.tasks]);
|
||||
|
||||
const getAvailableTools = () => {
|
||||
if (!values.config?.tasks || values.config.tasks.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const taskAgentIds = values.config.tasks.map(task => task.agent_id);
|
||||
|
||||
const toolsList: {id: string, name: string}[] = [];
|
||||
const toolsMap: Record<string, boolean> = {};
|
||||
|
||||
taskAgentIds.forEach(agentId => {
|
||||
const agent = agents.find(a => a.id === agentId);
|
||||
|
||||
if (agent?.type === "llm" && agent.config?.tools) {
|
||||
agent.config.tools.forEach(tool => {
|
||||
if (!toolsMap[tool.id]) {
|
||||
toolsList.push({ id: tool.id, name: tool.id });
|
||||
toolsMap[tool.id] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (agent?.type === "llm" && agent.config?.mcp_servers) {
|
||||
agent.config.mcp_servers.forEach(mcp => {
|
||||
if (mcp.tools) {
|
||||
mcp.tools.forEach(toolId => {
|
||||
if (!toolsMap[toolId]) {
|
||||
toolsList.push({ id: toolId, name: toolId });
|
||||
toolsMap[toolId] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return toolsList;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const availableTools = getAvailableTools();
|
||||
|
||||
if (toolSearchQuery.trim() === "") {
|
||||
setFilteredTools(availableTools);
|
||||
} else {
|
||||
const query = toolSearchQuery.toLowerCase();
|
||||
setFilteredTools(
|
||||
availableTools.filter(
|
||||
(tool) =>
|
||||
tool.name.toLowerCase().includes(query) ||
|
||||
tool.id.toLowerCase().includes(query)
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [toolSearchQuery, values.config?.tasks, agents]);
|
||||
|
||||
const handleAddTask = () => {
|
||||
if (!newTask.agent_id || !newTask.description) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEditing && editingTaskIndex !== null) {
|
||||
const tasks = [...(values.config?.tasks || [])];
|
||||
tasks[editingTaskIndex] = { ...newTask };
|
||||
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
tasks,
|
||||
},
|
||||
});
|
||||
|
||||
setIsEditing(false);
|
||||
setEditingTaskIndex(null);
|
||||
} else {
|
||||
const tasks = [...(values.config?.tasks || [])];
|
||||
|
||||
if (singleTask) {
|
||||
tasks.splice(0, tasks.length, newTask);
|
||||
} else {
|
||||
tasks.push(newTask);
|
||||
}
|
||||
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
tasks,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setNewTask({
|
||||
agent_id: "",
|
||||
description: "",
|
||||
expected_output: "",
|
||||
enabled_tools: [],
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditTask = (index: number) => {
|
||||
const task = values.config?.tasks?.[index];
|
||||
if (task) {
|
||||
setNewTask({ ...task });
|
||||
setIsEditing(true);
|
||||
setEditingTaskIndex(index);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setNewTask({
|
||||
agent_id: "",
|
||||
description: "",
|
||||
expected_output: "",
|
||||
enabled_tools: [],
|
||||
});
|
||||
setIsEditing(false);
|
||||
setEditingTaskIndex(null);
|
||||
};
|
||||
|
||||
const handleRemoveTask = (index: number) => {
|
||||
if (editingTaskIndex === index) {
|
||||
handleCancelEdit();
|
||||
}
|
||||
|
||||
const tasks = [...(values.config?.tasks || [])];
|
||||
tasks.splice(index, 1);
|
||||
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
tasks,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDescriptionChange = (
|
||||
e: React.ChangeEvent<HTMLTextAreaElement>
|
||||
) => {
|
||||
const newValue = e.target.value;
|
||||
setNewTask({
|
||||
...newTask,
|
||||
description: newValue,
|
||||
});
|
||||
};
|
||||
|
||||
const handleExpandDescription = () => {
|
||||
setExpandedDescription(newTask.description);
|
||||
setIsDescriptionModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveExpandedDescription = () => {
|
||||
setNewTask({
|
||||
...newTask,
|
||||
description: expandedDescription,
|
||||
});
|
||||
setIsDescriptionModalOpen(false);
|
||||
};
|
||||
|
||||
const handleToggleTool = (toolId: string) => {
|
||||
const index = tempSelectedTools.indexOf(toolId);
|
||||
|
||||
if (index > -1) {
|
||||
setTempSelectedTools(tempSelectedTools.filter(id => id !== toolId));
|
||||
} else {
|
||||
setTempSelectedTools([...tempSelectedTools, toolId]);
|
||||
}
|
||||
};
|
||||
|
||||
const isToolEnabled = (toolId: string) => {
|
||||
return tempSelectedTools.includes(toolId);
|
||||
};
|
||||
|
||||
const handleSaveTools = () => {
|
||||
if (isEditing && editingTaskIndex !== null && values.config?.tasks) {
|
||||
const tasks = [...(values.config?.tasks || [])];
|
||||
|
||||
const updatedTask = {
|
||||
...tasks[editingTaskIndex],
|
||||
enabled_tools: [...tempSelectedTools]
|
||||
};
|
||||
|
||||
tasks[editingTaskIndex] = updatedTask;
|
||||
|
||||
const newConfig = {
|
||||
...(values.config || {}),
|
||||
tasks: tasks
|
||||
};
|
||||
|
||||
onChange({
|
||||
...values,
|
||||
config: newConfig
|
||||
});
|
||||
|
||||
} else if (newTask.agent_id) {
|
||||
const updatedNewTask = {
|
||||
...newTask,
|
||||
enabled_tools: [...tempSelectedTools]
|
||||
};
|
||||
|
||||
setNewTask(updatedNewTask);
|
||||
}
|
||||
|
||||
setIsToolsModalOpen(false);
|
||||
};
|
||||
|
||||
const renderAgentTypeBadge = (agentId: string) => {
|
||||
const agent = agents.find((a) => a.id === agentId);
|
||||
if (!agent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge className={`ml-2 ${getAgentTypeColor(agent.type)} text-xs`}>
|
||||
{getAgentTypeLabel(agent.type)}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-medium text-white flex items-center">
|
||||
<List className="mr-2 h-5 w-5 text-emerald-400" />
|
||||
{singleTask ? "Task" : "Tasks"}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||
<p className="text-sm text-neutral-400 mb-4">
|
||||
{singleTask
|
||||
? "Configure the task that will be executed by the agent."
|
||||
: "Configure the sequential tasks that will be executed by the team of agents."}
|
||||
</p>
|
||||
|
||||
{values.config?.tasks && values.config.tasks.length > 0 ? (
|
||||
<div className="space-y-4 mb-4">
|
||||
{values.config.tasks.map((task, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`border border-[#333] rounded-md p-3 ${
|
||||
editingTaskIndex === index ? "bg-[#1e3a3a]" : "bg-[#2a2a2a]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center">
|
||||
<span className="inline-flex items-center justify-center rounded-full bg-[#333] px-2 py-1 text-xs text-white mr-2">
|
||||
{index + 1}
|
||||
</span>
|
||||
<h4 className="font-medium text-white flex items-center">
|
||||
{getAgentNameById(task.agent_id)}
|
||||
{renderAgentTypeBadge(task.agent_id)}
|
||||
</h4>
|
||||
</div>
|
||||
<p className="text-sm text-neutral-300 mt-1">
|
||||
{task.description}
|
||||
</p>
|
||||
{task.expected_output && (
|
||||
<div className="mt-2">
|
||||
<span className="text-xs text-neutral-400">
|
||||
Expected output:
|
||||
</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="ml-2 bg-[#333] text-emerald-400 border-emerald-400/30"
|
||||
>
|
||||
{task.expected_output}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{task.enabled_tools && task.enabled_tools.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<span className="text-xs text-neutral-400">
|
||||
Enabled tools:
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{task.enabled_tools.map((toolId) => (
|
||||
<Badge
|
||||
key={toolId}
|
||||
className="bg-[#333] text-emerald-400 border border-emerald-400/30 text-xs"
|
||||
>
|
||||
{toolId}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditTask(index)}
|
||||
className="text-neutral-400 hover:text-emerald-400 hover:bg-[#333] mr-1"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveTask(index)}
|
||||
className="text-red-500 hover:text-red-400 hover:bg-[#333]"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{!singleTask &&
|
||||
index < (values.config?.tasks?.length || 0) - 1 && (
|
||||
<div className="flex justify-center my-2">
|
||||
<ArrowDown className="h-4 w-4 text-neutral-400" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-4 mb-4 bg-[#2a2a2a] rounded-md">
|
||||
<p className="text-neutral-400">No tasks configured</p>
|
||||
<p className="text-xs text-neutral-500">
|
||||
{singleTask
|
||||
? "Add a task to define the agent's behavior"
|
||||
: "Add tasks to define the workflow of the team"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!singleTask ||
|
||||
!values.config?.tasks ||
|
||||
values.config.tasks.length === 0 ||
|
||||
isEditing) && (
|
||||
<div className="space-y-3 border-t border-[#333] pt-4">
|
||||
<h4 className="text-sm font-medium text-white flex items-center justify-between">
|
||||
<span>
|
||||
{isEditing
|
||||
? "Edit task"
|
||||
: `Add ${singleTask ? "one" : "new"} task`}
|
||||
</span>
|
||||
{isEditing && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleCancelEdit}
|
||||
className="text-neutral-400 hover:text-emerald-400 hover:bg-[#333]"
|
||||
>
|
||||
<X className="h-4 w-4 mr-1" /> Cancel
|
||||
</Button>
|
||||
)}
|
||||
</h4>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="agent_id"
|
||||
className="text-xs text-neutral-400 mb-1 block"
|
||||
>
|
||||
Agent
|
||||
</Label>
|
||||
<Select
|
||||
value={newTask.agent_id}
|
||||
onValueChange={(value) =>
|
||||
setNewTask({ ...newTask, agent_id: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="bg-[#2a2a2a] border-[#444] text-white">
|
||||
<SelectValue placeholder="Select agent" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#2a2a2a] border-[#444] text-white p-0">
|
||||
<div className="sticky top-0 z-10 p-2 bg-[#2a2a2a] border-b border-[#444]">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-neutral-400" />
|
||||
<Input
|
||||
placeholder="Search agents..."
|
||||
className="bg-[#333] border-[#444] text-white h-8 pl-8"
|
||||
value={taskAgentSearchQuery}
|
||||
onChange={(e) =>
|
||||
setTaskAgentSearchQuery(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[200px] overflow-y-auto py-1">
|
||||
{filteredTaskAgents.length > 0 ? (
|
||||
filteredTaskAgents.map((agent) => (
|
||||
<SelectItem
|
||||
key={agent.id}
|
||||
value={agent.id}
|
||||
className="hover:bg-[#333] focus:bg-[#333] flex items-center justify-between px-2"
|
||||
data-agent-item="true"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">{agent.name}</span>
|
||||
<Badge
|
||||
className={`${getAgentTypeColor(
|
||||
agent.type
|
||||
)} text-xs`}
|
||||
>
|
||||
{getAgentTypeLabel(agent.type)}
|
||||
</Badge>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<div className="text-neutral-500 px-4 py-2 text-center">
|
||||
No agents found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<Label
|
||||
htmlFor="description"
|
||||
className="text-xs text-neutral-400 mb-1 block"
|
||||
>
|
||||
Task description
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
id="description"
|
||||
value={newTask.description}
|
||||
onChange={handleDescriptionChange}
|
||||
className="w-full bg-[#2a2a2a] border-[#444] text-white pr-10"
|
||||
rows={3}
|
||||
onClick={handleExpandDescription}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-3 right-5 text-neutral-400 hover:text-emerald-400 focus:outline-none"
|
||||
onClick={handleExpandDescription}
|
||||
>
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-neutral-400">
|
||||
<span className="inline-block h-3 w-3 mr-1">ℹ️</span>
|
||||
<span>
|
||||
Use {"{"}content{"}"} to insert the user's input.
|
||||
<span className="ml-2 text-emerald-400">
|
||||
Click to expand editor.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="expected_output"
|
||||
className="text-xs text-neutral-400 mb-1 block"
|
||||
>
|
||||
Expected output (optional)
|
||||
</Label>
|
||||
<Input
|
||||
id="expected_output"
|
||||
placeholder="Ex: JSON report, List of recommendations, etc."
|
||||
value={newTask.expected_output}
|
||||
onChange={(e) =>
|
||||
setNewTask({ ...newTask, expected_output: e.target.value })
|
||||
}
|
||||
className="bg-[#2a2a2a] border-[#444] text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{newTask.enabled_tools && newTask.enabled_tools.length > 0 && (
|
||||
<div className="mt-3">
|
||||
<Label className="text-xs text-neutral-400 mb-1 block">
|
||||
Selected tools:
|
||||
</Label>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{newTask.enabled_tools.map((toolId) => (
|
||||
<Badge
|
||||
key={toolId}
|
||||
className="bg-[#333] text-emerald-400 border border-emerald-400/30"
|
||||
>
|
||||
{toolId}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (newTask.agent_id) setIsToolsModalOpen(true);
|
||||
}}
|
||||
disabled={!newTask.agent_id}
|
||||
className="border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 px-3"
|
||||
>
|
||||
<PenTool className="h-4 w-4 mr-2" />
|
||||
Configure tools
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleAddTask}
|
||||
disabled={!newTask.agent_id || !newTask.description}
|
||||
className="bg-[#222] text-emerald-400 border border-emerald-400 hover:bg-emerald-400/10"
|
||||
>
|
||||
<Save className="h-4 w-4 mr-1" />{" "}
|
||||
{isEditing ? "Update task" : "Add task"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
open={isDescriptionModalOpen}
|
||||
onOpenChange={setIsDescriptionModalOpen}
|
||||
>
|
||||
<DialogContent className="sm:max-w-[1200px] max-h-[90vh] bg-[#1a1a1a] border-[#333] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Task Description</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-hidden flex flex-col min-h-[60vh]">
|
||||
<Textarea
|
||||
value={expandedDescription}
|
||||
onChange={(e) => setExpandedDescription(e.target.value)}
|
||||
className="flex-1 min-h-full bg-[#222] border-[#444] text-white p-4 focus:border-emerald-400 focus:ring-emerald-400 focus:ring-opacity-50 resize-none"
|
||||
placeholder="Enter detailed description for the task..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsDescriptionModalOpen(false)}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveExpandedDescription}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Save description
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={isToolsModalOpen} onOpenChange={setIsToolsModalOpen}>
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[90vh] bg-[#1a1a1a] border-[#333] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">
|
||||
Available tools
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="relative mb-4">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-neutral-400" />
|
||||
<Input
|
||||
placeholder="Search tools..."
|
||||
className="bg-[#222] border-[#444] text-white pl-9"
|
||||
value={toolSearchQuery}
|
||||
onChange={(e) => setToolSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto space-y-2 pr-1">
|
||||
{filteredTools.length > 0 ? (
|
||||
filteredTools.map((tool) => (
|
||||
<div
|
||||
key={tool.id}
|
||||
className="flex items-center space-x-2 p-2 rounded-md hover:bg-[#333] transition duration-150"
|
||||
>
|
||||
<Checkbox
|
||||
id={tool.id}
|
||||
checked={isToolEnabled(tool.id)}
|
||||
onCheckedChange={() => handleToggleTool(tool.id)}
|
||||
className="border-[#444] data-[state=checked]:bg-emerald-400 data-[state=checked]:text-black"
|
||||
/>
|
||||
<Label
|
||||
htmlFor={tool.id}
|
||||
className="cursor-pointer text-white flex-1"
|
||||
>
|
||||
{tool.name}
|
||||
</Label>
|
||||
<Badge className="bg-[#333] text-emerald-400">{tool.id}</Badge>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-neutral-400">No tools available</p>
|
||||
<p className="text-xs text-neutral-500">
|
||||
The tools are obtained from the selected agents in the tasks.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={handleSaveTools}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Save settings
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
184
frontend/app/agents/dialogs/AgentToolDialog.tsx
Normal file
184
frontend/app/agents/dialogs/AgentToolDialog.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/dialogs/AgentToolDialog.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 { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { listAgents } from "@/services/agentService";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
interface AgentToolDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSave: (tool: { id: string; envs: Record<string, string> }) => void;
|
||||
currentAgentId?: string;
|
||||
folderId?: string | null;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
export function AgentToolDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSave,
|
||||
currentAgentId,
|
||||
folderId,
|
||||
clientId,
|
||||
}: AgentToolDialogProps) {
|
||||
const [selectedAgentId, setSelectedAgentId] = useState<string>("");
|
||||
const [search, setSearch] = useState("");
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setSelectedAgentId("");
|
||||
setSearch("");
|
||||
loadAgents();
|
||||
}
|
||||
}, [open, folderId, clientId]);
|
||||
|
||||
const loadAgents = async () => {
|
||||
if (!clientId) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await listAgents(
|
||||
clientId,
|
||||
0,
|
||||
100,
|
||||
folderId || undefined
|
||||
);
|
||||
|
||||
// Filter out the current agent to avoid self-reference
|
||||
const filteredAgents = res.data.filter(agent =>
|
||||
agent.id !== currentAgentId
|
||||
);
|
||||
|
||||
setAgents(filteredAgents);
|
||||
} catch (error) {
|
||||
console.error("Error loading agents:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!selectedAgentId) return;
|
||||
onSave({ id: selectedAgentId, envs: {} });
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const filteredAgents = agents.filter((agent) =>
|
||||
agent.name.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[420px] max-h-[90vh] overflow-hidden flex flex-col bg-[#1a1a1a] border-[#333]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Add Agent Tool</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
Select an agent to add as a tool.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-auto px-2 pb-2 space-y-4">
|
||||
<Input
|
||||
placeholder="Search agent by name..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="mb-2 bg-[#222] border-[#444] text-white placeholder:text-neutral-400"
|
||||
/>
|
||||
<div className="space-y-2 max-h-60 overflow-y-auto pr-1">
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-6">
|
||||
<Loader2 className="h-6 w-6 text-emerald-400 animate-spin" />
|
||||
<div className="mt-2 text-sm text-neutral-400">Loading agents...</div>
|
||||
</div>
|
||||
) : filteredAgents.length === 0 ? (
|
||||
<div className="text-neutral-400 text-sm text-center py-6">
|
||||
{search ? `No agents found matching "${search}"` : "No agents found in this folder."}
|
||||
</div>
|
||||
) : (
|
||||
filteredAgents.map((agent) => (
|
||||
<button
|
||||
key={agent.id}
|
||||
type="button"
|
||||
onClick={() => setSelectedAgentId(agent.id)}
|
||||
className={cn(
|
||||
"w-full flex items-start gap-3 p-3 rounded-md border border-[#333] bg-[#232323] hover:bg-[#222] transition text-left cursor-pointer",
|
||||
selectedAgentId === agent.id && "border-emerald-400 bg-[#1a1a1a] shadow-md"
|
||||
)}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-white text-base">{agent.name}</div>
|
||||
<div className="text-xs text-neutral-400 mt-1">
|
||||
{agent.description || "No description"}
|
||||
</div>
|
||||
<div className="text-[10px] text-neutral-500 mt-1">ID: {agent.id}</div>
|
||||
</div>
|
||||
{selectedAgentId === agent.id && (
|
||||
<span className="ml-2 text-emerald-400 font-bold">Selected</span>
|
||||
)}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="p-4 pt-2 border-t border-[#333]">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
disabled={!selectedAgentId || isLoading}
|
||||
>
|
||||
Add Tool
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
445
frontend/app/agents/dialogs/ApiKeysDialog.tsx
Normal file
445
frontend/app/agents/dialogs/ApiKeysDialog.tsx
Normal file
@@ -0,0 +1,445 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/dialogs/ApiKeysDialog.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 { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { ConfirmationDialog } from "./ConfirmationDialog";
|
||||
import { ApiKey } from "@/services/agentService";
|
||||
import { Edit, Eye, Key, Plus, Trash2, X } from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { availableModelProviders } from "@/types/aiModels";
|
||||
|
||||
interface ApiKeysDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
apiKeys: ApiKey[];
|
||||
isLoading: boolean;
|
||||
onAddApiKey: (apiKey: {
|
||||
name: string;
|
||||
provider: string;
|
||||
key_value: string;
|
||||
}) => Promise<void>;
|
||||
onUpdateApiKey: (
|
||||
id: string,
|
||||
apiKey: {
|
||||
name: string;
|
||||
provider: string;
|
||||
key_value?: string;
|
||||
is_active: boolean;
|
||||
}
|
||||
) => Promise<void>;
|
||||
onDeleteApiKey: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function ApiKeysDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
apiKeys,
|
||||
isLoading,
|
||||
onAddApiKey,
|
||||
onUpdateApiKey,
|
||||
onDeleteApiKey,
|
||||
}: ApiKeysDialogProps) {
|
||||
const [isAddingApiKey, setIsAddingApiKey] = useState(false);
|
||||
const [isEditingApiKey, setIsEditingApiKey] = useState(false);
|
||||
const [currentApiKey, setCurrentApiKey] = useState<
|
||||
Partial<ApiKey & { key_value?: string }>
|
||||
>({});
|
||||
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [apiKeyToDelete, setApiKeyToDelete] = useState<ApiKey | null>(null);
|
||||
const [isApiKeyVisible, setIsApiKeyVisible] = useState(false);
|
||||
|
||||
const handleAddClick = () => {
|
||||
setCurrentApiKey({});
|
||||
setIsAddingApiKey(true);
|
||||
setIsEditingApiKey(false);
|
||||
};
|
||||
|
||||
const handleEditClick = (apiKey: ApiKey) => {
|
||||
setCurrentApiKey({ ...apiKey, key_value: "" });
|
||||
setIsAddingApiKey(true);
|
||||
setIsEditingApiKey(true);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (apiKey: ApiKey) => {
|
||||
setApiKeyToDelete(apiKey);
|
||||
setIsDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveApiKey = async () => {
|
||||
if (
|
||||
!currentApiKey.name ||
|
||||
!currentApiKey.provider ||
|
||||
(!isEditingApiKey && !currentApiKey.key_value)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (currentApiKey.id) {
|
||||
await onUpdateApiKey(currentApiKey.id, {
|
||||
name: currentApiKey.name,
|
||||
provider: currentApiKey.provider,
|
||||
key_value: currentApiKey.key_value,
|
||||
is_active: currentApiKey.is_active !== false,
|
||||
});
|
||||
} else {
|
||||
await onAddApiKey({
|
||||
name: currentApiKey.name,
|
||||
provider: currentApiKey.provider,
|
||||
key_value: currentApiKey.key_value!,
|
||||
});
|
||||
}
|
||||
|
||||
setCurrentApiKey({});
|
||||
setIsAddingApiKey(false);
|
||||
setIsEditingApiKey(false);
|
||||
} catch (error) {
|
||||
console.error("Error saving API key:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!apiKeyToDelete) return;
|
||||
|
||||
try {
|
||||
await onDeleteApiKey(apiKeyToDelete.id);
|
||||
setApiKeyToDelete(null);
|
||||
setIsDeleteDialogOpen(false);
|
||||
} catch (error) {
|
||||
console.error("Error deleting API key:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-hidden flex flex-col bg-[#1a1a1a] border-[#333]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Manage API Keys</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
Add and manage API keys for use in your agents
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-auto p-1">
|
||||
{isAddingApiKey ? (
|
||||
<div className="space-y-4 p-4 bg-[#222] rounded-md">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-medium text-white">
|
||||
{isEditingApiKey ? "Edit Key" : "New Key"}
|
||||
</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsAddingApiKey(false);
|
||||
setIsEditingApiKey(false);
|
||||
setCurrentApiKey({});
|
||||
}}
|
||||
className="text-neutral-400 hover:text-white"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right text-neutral-300">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={currentApiKey.name || ""}
|
||||
onChange={(e) =>
|
||||
setCurrentApiKey({
|
||||
...currentApiKey,
|
||||
name: e.target.value,
|
||||
})
|
||||
}
|
||||
className="col-span-3 bg-[#333] border-[#444] text-white"
|
||||
placeholder="OpenAI GPT-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label
|
||||
htmlFor="provider"
|
||||
className="text-right text-neutral-300"
|
||||
>
|
||||
Provider
|
||||
</Label>
|
||||
<Select
|
||||
value={currentApiKey.provider}
|
||||
onValueChange={(value) =>
|
||||
setCurrentApiKey({
|
||||
...currentApiKey,
|
||||
provider: value,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="col-span-3 bg-[#333] border-[#444] text-white">
|
||||
<SelectValue placeholder="Select Provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#222] border-[#444] text-white">
|
||||
{availableModelProviders.map((provider) => (
|
||||
<SelectItem
|
||||
key={provider.value}
|
||||
value={provider.value}
|
||||
className="data-[selected]:bg-[#333] data-[highlighted]:bg-[#333] !text-white focus:!text-white hover:text-emerald-400 data-[selected]:!text-emerald-400"
|
||||
>
|
||||
{provider.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label
|
||||
htmlFor="key_value"
|
||||
className="text-right text-neutral-300"
|
||||
>
|
||||
Key Value
|
||||
</Label>
|
||||
<div className="col-span-3 relative">
|
||||
<Input
|
||||
id="key_value"
|
||||
value={currentApiKey.key_value || ""}
|
||||
onChange={(e) =>
|
||||
setCurrentApiKey({
|
||||
...currentApiKey,
|
||||
key_value: e.target.value,
|
||||
})
|
||||
}
|
||||
className="bg-[#333] border-[#444] text-white pr-10"
|
||||
type={isApiKeyVisible ? "text" : "password"}
|
||||
placeholder={
|
||||
isEditingApiKey
|
||||
? "Leave blank to keep the current value"
|
||||
: "sk-..."
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
className="absolute right-2 top-1/2 transform -translate-y-1/2 h-7 w-7 p-0 text-neutral-400 hover:text-white"
|
||||
onClick={() => setIsApiKeyVisible(!isApiKeyVisible)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isEditingApiKey && (
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label
|
||||
htmlFor="is_active"
|
||||
className="text-right text-neutral-300"
|
||||
>
|
||||
Status
|
||||
</Label>
|
||||
<div className="col-span-3 flex items-center">
|
||||
<Checkbox
|
||||
id="is_active"
|
||||
checked={currentApiKey.is_active !== false}
|
||||
onCheckedChange={(checked) =>
|
||||
setCurrentApiKey({
|
||||
...currentApiKey,
|
||||
is_active: !!checked,
|
||||
})
|
||||
}
|
||||
className="mr-2 data-[state=checked]:bg-emerald-400 data-[state=checked]:border-emerald-400"
|
||||
/>
|
||||
<Label htmlFor="is_active" className="text-neutral-300">
|
||||
Active
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsAddingApiKey(false);
|
||||
setIsEditingApiKey(false);
|
||||
setCurrentApiKey({});
|
||||
}}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveApiKey}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="animate-spin h-4 w-4 border-2 border-black border-t-transparent rounded-full mr-1"></div>
|
||||
)}
|
||||
{isEditingApiKey ? "Update" : "Add"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium text-white">
|
||||
Available Keys
|
||||
</h3>
|
||||
<Button
|
||||
onClick={handleAddClick}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Key
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-40">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-emerald-400"></div>
|
||||
</div>
|
||||
) : apiKeys.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{apiKeys.map((apiKey) => (
|
||||
<div
|
||||
key={apiKey.id}
|
||||
className="flex items-center justify-between p-3 bg-[#222] rounded-md border border-[#333] hover:border-emerald-400/30"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium text-white">{apiKey.name}</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-[#333] text-emerald-400 border-emerald-400/30"
|
||||
>
|
||||
{apiKey.provider.toUpperCase()}
|
||||
</Badge>
|
||||
<p className="text-xs text-neutral-400">
|
||||
Created on{" "}
|
||||
{new Date(apiKey.created_at).toLocaleDateString()}
|
||||
</p>
|
||||
{!apiKey.is_active && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-[#333] text-red-400 border-red-400/30"
|
||||
>
|
||||
Inactive
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditClick(apiKey)}
|
||||
className="text-neutral-300 hover:text-emerald-400 hover:bg-[#333]"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteClick(apiKey)}
|
||||
className="text-red-500 hover:text-red-400 hover:bg-[#333]"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-10 border border-dashed border-[#333] rounded-md bg-[#222] text-neutral-400">
|
||||
<Key className="mx-auto h-10 w-10 text-neutral-500 mb-3" />
|
||||
<p>You don't have any API keys registered</p>
|
||||
<p className="text-sm mt-1">
|
||||
Add your API keys to use them in your agents
|
||||
</p>
|
||||
<Button
|
||||
onClick={handleAddClick}
|
||||
className="mt-4 bg-[#333] text-emerald-400 hover:bg-[#444]"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Key
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="border-t border-[#333] pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
<ConfirmationDialog
|
||||
open={isDeleteDialogOpen}
|
||||
onOpenChange={setIsDeleteDialogOpen}
|
||||
title="Confirm Delete"
|
||||
description={`Are you sure you want to delete the key "${apiKeyToDelete?.name}"? This action cannot be undone.`}
|
||||
confirmText="Delete"
|
||||
confirmVariant="destructive"
|
||||
onConfirm={handleDeleteConfirm}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
93
frontend/app/agents/dialogs/ConfirmationDialog.tsx
Normal file
93
frontend/app/agents/dialogs/ConfirmationDialog.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/dialogs/ConfirmationDialog.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 {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
interface ConfirmationDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
title: string;
|
||||
description: string;
|
||||
cancelText?: string;
|
||||
confirmText?: string;
|
||||
confirmVariant?: "default" | "destructive";
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
export function ConfirmationDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
title,
|
||||
description,
|
||||
cancelText = "Cancel",
|
||||
confirmText = "Confirm",
|
||||
confirmVariant = "default",
|
||||
onConfirm,
|
||||
}: ConfirmationDialogProps) {
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent className="bg-[#1a1a1a] border-[#333] text-white">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-neutral-400">
|
||||
{description}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white">
|
||||
{cancelText}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onConfirm();
|
||||
}}
|
||||
className={
|
||||
confirmVariant === "destructive"
|
||||
? "bg-red-600 text-white hover:bg-red-700"
|
||||
: "bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
}
|
||||
>
|
||||
{confirmText}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
237
frontend/app/agents/dialogs/CustomMCPDialog.tsx
Normal file
237
frontend/app/agents/dialogs/CustomMCPDialog.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/dialogs/CustomMCPDialog.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 { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { X, Plus } from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface CustomMCPHeader {
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface CustomMCPServer {
|
||||
url: string;
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
|
||||
interface CustomMCPDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSave: (customMCP: CustomMCPServer) => void;
|
||||
initialCustomMCP?: CustomMCPServer | null;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
export function CustomMCPDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSave,
|
||||
initialCustomMCP = null,
|
||||
clientId,
|
||||
}: CustomMCPDialogProps) {
|
||||
const [customMCP, setCustomMCP] = useState<Partial<CustomMCPServer>>({
|
||||
url: "",
|
||||
headers: {},
|
||||
});
|
||||
|
||||
const [headersList, setHeadersList] = useState<CustomMCPHeader[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (initialCustomMCP) {
|
||||
setCustomMCP(initialCustomMCP);
|
||||
const headersList = Object.entries(initialCustomMCP.headers || {}).map(
|
||||
([key, value], index) => ({
|
||||
id: `header-${index}`,
|
||||
key,
|
||||
value,
|
||||
})
|
||||
);
|
||||
setHeadersList(headersList);
|
||||
} else {
|
||||
setCustomMCP({ url: "", headers: {} });
|
||||
setHeadersList([]);
|
||||
}
|
||||
}
|
||||
}, [open, initialCustomMCP]);
|
||||
|
||||
const handleAddHeader = () => {
|
||||
setHeadersList([
|
||||
...headersList,
|
||||
{ id: `header-${Date.now()}`, key: "", value: "" },
|
||||
]);
|
||||
};
|
||||
|
||||
const handleRemoveHeader = (id: string) => {
|
||||
setHeadersList(headersList.filter((header) => header.id !== id));
|
||||
};
|
||||
|
||||
const handleHeaderChange = (
|
||||
id: string,
|
||||
field: "key" | "value",
|
||||
value: string
|
||||
) => {
|
||||
setHeadersList(
|
||||
headersList.map((header) =>
|
||||
header.id === id ? { ...header, [field]: value } : header
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!customMCP.url) return;
|
||||
|
||||
const headersObject: Record<string, string> = {};
|
||||
headersList.forEach((header) => {
|
||||
if (header.key.trim()) {
|
||||
headersObject[header.key] = header.value;
|
||||
}
|
||||
});
|
||||
|
||||
onSave({
|
||||
url: customMCP.url,
|
||||
headers: headersObject,
|
||||
});
|
||||
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-hidden flex flex-col bg-[#1a1a1a] border-[#333]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">
|
||||
Configure Custom MCP
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
Configure the URL and HTTP headers for the custom MCP.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="custom-mcp-url" className="text-neutral-300">
|
||||
MCP URL
|
||||
</Label>
|
||||
<Input
|
||||
id="custom-mcp-url"
|
||||
value={customMCP.url || ""}
|
||||
onChange={(e) =>
|
||||
setCustomMCP({
|
||||
...customMCP,
|
||||
url: e.target.value,
|
||||
})
|
||||
}
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
placeholder="https://meu-servidor-mcp.com/api"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-white">HTTP Headers</h3>
|
||||
<div className="border border-[#444] rounded-md p-3 bg-[#222]">
|
||||
{headersList.map((header) => (
|
||||
<div
|
||||
key={header.id}
|
||||
className="grid grid-cols-5 items-center gap-2 mb-2"
|
||||
>
|
||||
<Input
|
||||
value={header.key}
|
||||
onChange={(e) =>
|
||||
handleHeaderChange(header.id, "key", e.target.value)
|
||||
}
|
||||
className="col-span-2 bg-[#333] border-[#444] text-white"
|
||||
placeholder="Header Name"
|
||||
/>
|
||||
<Input
|
||||
value={header.value}
|
||||
onChange={(e) =>
|
||||
handleHeaderChange(header.id, "value", e.target.value)
|
||||
}
|
||||
className="col-span-2 bg-[#333] border-[#444] text-white"
|
||||
placeholder="Header Value"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveHeader(header.id)}
|
||||
className="col-span-1 h-8 text-red-500 hover:text-red-400 hover:bg-[#444]"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddHeader}
|
||||
className="w-full mt-2 border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add Header
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="p-4 pt-2 border-t border-[#333]">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
disabled={!customMCP.url}
|
||||
>
|
||||
{initialCustomMCP?.url ? "Save Custom MCP" : "Add Custom MCP"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
864
frontend/app/agents/dialogs/CustomToolDialog.tsx
Normal file
864
frontend/app/agents/dialogs/CustomToolDialog.tsx
Normal file
@@ -0,0 +1,864 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/dialogs/CustomToolDialog.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 { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
X,
|
||||
Plus,
|
||||
Info,
|
||||
Trash,
|
||||
Globe,
|
||||
FileJson,
|
||||
LayoutList,
|
||||
Settings,
|
||||
Database,
|
||||
Code,
|
||||
Server,
|
||||
Wand
|
||||
} from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { HTTPTool, HTTPToolParameter } from "@/types/agent";
|
||||
import { sanitizeAgentName } from "@/lib/utils";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
interface CustomToolDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSave: (tool: HTTPTool) => void;
|
||||
initialTool?: HTTPTool | null;
|
||||
}
|
||||
|
||||
export function CustomToolDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSave,
|
||||
initialTool = null,
|
||||
}: CustomToolDialogProps) {
|
||||
const [tool, setTool] = useState<Partial<HTTPTool>>({
|
||||
name: "",
|
||||
method: "GET",
|
||||
endpoint: "",
|
||||
description: "",
|
||||
headers: {},
|
||||
values: {},
|
||||
parameters: {},
|
||||
error_handling: {
|
||||
timeout: 30,
|
||||
retry_count: 0,
|
||||
fallback_response: {},
|
||||
},
|
||||
});
|
||||
|
||||
const [headersList, setHeadersList] = useState<{ id: string; key: string; value: string }[]>([]);
|
||||
const [bodyParams, setBodyParams] = useState<{ id: string; key: string; param: HTTPToolParameter }[]>([]);
|
||||
const [pathParams, setPathParams] = useState<{ id: string; key: string; desc: string }[]>([]);
|
||||
const [queryParams, setQueryParams] = useState<{ id: string; key: string; value: string }[]>([]);
|
||||
const [valuesList, setValuesList] = useState<{ id: string; key: string; value: string }[]>([]);
|
||||
const [timeout, setTimeout] = useState<number>(30);
|
||||
const [fallbackError, setFallbackError] = useState<string>("");
|
||||
const [fallbackMessage, setFallbackMessage] = useState<string>("");
|
||||
const [activeTab, setActiveTab] = useState("basics");
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (initialTool) {
|
||||
setTool(initialTool);
|
||||
setHeadersList(
|
||||
Object.entries(initialTool.headers || {}).map(([key, value], idx) => ({
|
||||
id: `header-${idx}`,
|
||||
key,
|
||||
value,
|
||||
}))
|
||||
);
|
||||
setBodyParams(
|
||||
Object.entries(initialTool.parameters?.body_params || {}).map(([key, param], idx) => ({
|
||||
id: `body-${idx}`,
|
||||
key,
|
||||
param,
|
||||
}))
|
||||
);
|
||||
setPathParams(
|
||||
Object.entries(initialTool.parameters?.path_params || {}).map(([key, desc], idx) => ({
|
||||
id: `path-${idx}`,
|
||||
key,
|
||||
desc: desc as string,
|
||||
}))
|
||||
);
|
||||
setQueryParams(
|
||||
Object.entries(initialTool.parameters?.query_params || {}).map(([key, value], idx) => ({
|
||||
id: `query-${idx}`,
|
||||
key,
|
||||
value: value as string,
|
||||
}))
|
||||
);
|
||||
setValuesList(
|
||||
Object.entries(initialTool.values || {}).map(([key, value], idx) => ({
|
||||
id: `val-${idx}`,
|
||||
key,
|
||||
value: value as string,
|
||||
}))
|
||||
);
|
||||
setTimeout(initialTool.error_handling?.timeout || 30);
|
||||
setFallbackError(initialTool.error_handling?.fallback_response?.error || "");
|
||||
setFallbackMessage(initialTool.error_handling?.fallback_response?.message || "");
|
||||
} else {
|
||||
setTool({
|
||||
name: "",
|
||||
method: "GET",
|
||||
endpoint: "",
|
||||
description: "",
|
||||
headers: {},
|
||||
values: {},
|
||||
parameters: {},
|
||||
error_handling: {
|
||||
timeout: 30,
|
||||
retry_count: 0,
|
||||
fallback_response: {},
|
||||
},
|
||||
});
|
||||
setHeadersList([]);
|
||||
setBodyParams([]);
|
||||
setPathParams([]);
|
||||
setQueryParams([]);
|
||||
setValuesList([]);
|
||||
setTimeout(30);
|
||||
setFallbackError("");
|
||||
setFallbackMessage("");
|
||||
}
|
||||
setActiveTab("basics");
|
||||
}
|
||||
}, [open, initialTool]);
|
||||
|
||||
const handleAddHeader = () => {
|
||||
setHeadersList([...headersList, { id: `header-${Date.now()}`, key: "", value: "" }]);
|
||||
};
|
||||
const handleRemoveHeader = (id: string) => {
|
||||
setHeadersList(headersList.filter((h) => h.id !== id));
|
||||
};
|
||||
const handleHeaderChange = (id: string, field: "key" | "value", value: string) => {
|
||||
setHeadersList(headersList.map((h) => (h.id === id ? { ...h, [field]: value } : h)));
|
||||
};
|
||||
|
||||
const handleAddBodyParam = () => {
|
||||
setBodyParams([
|
||||
...bodyParams,
|
||||
{
|
||||
id: `body-${Date.now()}`,
|
||||
key: "",
|
||||
param: { type: "string", required: false, description: "" },
|
||||
},
|
||||
]);
|
||||
};
|
||||
const handleRemoveBodyParam = (id: string) => {
|
||||
setBodyParams(bodyParams.filter((p) => p.id !== id));
|
||||
};
|
||||
const handleBodyParamChange = (id: string, field: "key" | keyof HTTPToolParameter, value: string | boolean) => {
|
||||
setBodyParams(
|
||||
bodyParams.map((p) =>
|
||||
p.id === id
|
||||
? field === "key"
|
||||
? { ...p, key: value as string }
|
||||
: { ...p, param: { ...p.param, [field]: value } }
|
||||
: p
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// Path Params
|
||||
const handleAddPathParam = () => {
|
||||
setPathParams([...pathParams, { id: `path-${Date.now()}`, key: "", desc: "" }]);
|
||||
};
|
||||
const handleRemovePathParam = (id: string) => {
|
||||
setPathParams(pathParams.filter((p) => p.id !== id));
|
||||
};
|
||||
const handlePathParamChange = (id: string, field: "key" | "desc", value: string) => {
|
||||
setPathParams(pathParams.map((p) => (p.id === id ? { ...p, [field]: value } : p)));
|
||||
};
|
||||
|
||||
// Query Params
|
||||
const handleAddQueryParam = () => {
|
||||
setQueryParams([...queryParams, { id: `query-${Date.now()}`, key: "", value: "" }]);
|
||||
};
|
||||
const handleRemoveQueryParam = (id: string) => {
|
||||
setQueryParams(queryParams.filter((q) => q.id !== id));
|
||||
};
|
||||
const handleQueryParamChange = (id: string, field: "key" | "value", value: string) => {
|
||||
setQueryParams(queryParams.map((q) => (q.id === id ? { ...q, [field]: value } : q)));
|
||||
};
|
||||
|
||||
// Values
|
||||
const handleAddValue = () => {
|
||||
setValuesList([...valuesList, { id: `val-${Date.now()}`, key: "", value: "" }]);
|
||||
};
|
||||
const handleRemoveValue = (id: string) => {
|
||||
setValuesList(valuesList.filter((v) => v.id !== id));
|
||||
};
|
||||
const handleValueChange = (id: string, field: "key" | "value", value: string) => {
|
||||
setValuesList(valuesList.map((v) => (v.id === id ? { ...v, [field]: value } : v)));
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!tool.name || !tool.endpoint) return;
|
||||
const headersObject: Record<string, string> = {};
|
||||
headersList.forEach((h) => {
|
||||
if (h.key.trim()) headersObject[h.key] = h.value;
|
||||
});
|
||||
const bodyParamsObject: Record<string, HTTPToolParameter> = {};
|
||||
bodyParams.forEach((p) => {
|
||||
if (p.key.trim()) bodyParamsObject[p.key] = p.param;
|
||||
});
|
||||
const pathParamsObject: Record<string, string> = {};
|
||||
pathParams.forEach((p) => {
|
||||
if (p.key.trim()) pathParamsObject[p.key] = p.desc;
|
||||
});
|
||||
const queryParamsObject: Record<string, string> = {};
|
||||
queryParams.forEach((q) => {
|
||||
if (q.key.trim()) queryParamsObject[q.key] = q.value;
|
||||
});
|
||||
const valuesObject: Record<string, string> = {};
|
||||
valuesList.forEach((v) => {
|
||||
if (v.key.trim()) valuesObject[v.key] = v.value;
|
||||
});
|
||||
|
||||
// Sanitize the tool name
|
||||
const sanitizedName = sanitizeAgentName(tool.name);
|
||||
|
||||
onSave({
|
||||
...(tool as HTTPTool),
|
||||
name: sanitizedName,
|
||||
headers: headersObject,
|
||||
values: valuesObject,
|
||||
parameters: {
|
||||
...tool.parameters,
|
||||
body_params: bodyParamsObject,
|
||||
path_params: pathParamsObject,
|
||||
query_params: queryParamsObject,
|
||||
},
|
||||
error_handling: {
|
||||
timeout,
|
||||
retry_count: tool.error_handling?.retry_count ?? 0,
|
||||
fallback_response: {
|
||||
error: fallbackError,
|
||||
message: fallbackMessage,
|
||||
},
|
||||
},
|
||||
} as HTTPTool);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const ParamField = ({
|
||||
children,
|
||||
label,
|
||||
tooltip
|
||||
}: {
|
||||
children: React.ReactNode,
|
||||
label: string,
|
||||
tooltip?: string
|
||||
}) => (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label className="text-sm font-medium text-neutral-200">
|
||||
{label}
|
||||
</Label>
|
||||
{tooltip && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-3.5 w-3.5 text-neutral-400 cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-neutral-800 border-neutral-700 text-white p-3 max-w-sm">
|
||||
<p className="text-xs">{tooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const FieldList = <T extends Record<string, any>>({
|
||||
items,
|
||||
onAdd,
|
||||
onRemove,
|
||||
onChange,
|
||||
fields,
|
||||
addText,
|
||||
emptyText,
|
||||
icon
|
||||
}: {
|
||||
items: T[],
|
||||
onAdd: () => void,
|
||||
onRemove: (id: string) => void,
|
||||
onChange: (id: string, field: string, value: any) => void,
|
||||
fields: { name: string, field: string, placeholder: string, width: number, type?: string }[],
|
||||
addText: string,
|
||||
emptyText: string,
|
||||
icon: React.ReactNode
|
||||
}) => (
|
||||
<div className="border border-neutral-700 rounded-md p-3 bg-neutral-800/50">
|
||||
{items.length > 0 ? (
|
||||
<div className="space-y-2 mb-3">
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="flex items-center gap-2 group">
|
||||
<div className="flex-1 flex gap-2 w-full">
|
||||
{fields.map(field => {
|
||||
// Calculate percentage width based on the field's width value
|
||||
const widthPercent = (field.width / 12) * 100;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={field.name}
|
||||
className="flex-shrink-0"
|
||||
style={{ width: `${widthPercent}%` }}
|
||||
>
|
||||
{field.type === 'select' ? (
|
||||
<select
|
||||
value={(field.field.includes('.')
|
||||
? item.param?.[field.field.split('.')[1]]
|
||||
: item[field.field]) || ''}
|
||||
onChange={(e) => onChange(
|
||||
item.id,
|
||||
field.field,
|
||||
e.target.value
|
||||
)}
|
||||
className="w-full h-9 px-3 py-1 rounded-md bg-neutral-900 border border-neutral-700 text-white text-sm"
|
||||
>
|
||||
<option value="string">string</option>
|
||||
<option value="number">number</option>
|
||||
<option value="boolean">boolean</option>
|
||||
</select>
|
||||
) : field.type === 'checkbox' ? (
|
||||
<div className="flex items-center h-9 w-full justify-center">
|
||||
<label className="flex items-center gap-1.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.param?.required || false}
|
||||
onChange={(e) => onChange(
|
||||
item.id,
|
||||
field.field,
|
||||
(e.target as HTMLInputElement).checked
|
||||
)}
|
||||
className="accent-emerald-400 rounded"
|
||||
/>
|
||||
<span className="text-xs text-neutral-300">Required</span>
|
||||
</label>
|
||||
</div>
|
||||
) : (
|
||||
<Input
|
||||
value={(field.field.includes('.')
|
||||
? item.param?.[field.field.split('.')[1]]
|
||||
: item[field.field]) || ''}
|
||||
onChange={(e) => onChange(
|
||||
item.id,
|
||||
field.field,
|
||||
e.target.value
|
||||
)}
|
||||
className="h-9 w-full bg-neutral-900 border-neutral-700 text-white placeholder:text-neutral-500 text-sm"
|
||||
placeholder={field.placeholder}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onRemove(item.id)}
|
||||
className="h-9 w-9 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity text-red-400 hover:text-red-300 hover:bg-red-900/20"
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-6 flex flex-col items-center justify-center text-center">
|
||||
{icon}
|
||||
<p className="mt-2 text-neutral-400 text-sm">{emptyText}</p>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onAdd}
|
||||
className="w-full border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/10 bg-neutral-800/30"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1.5" /> {addText}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[850px] max-h-[90vh] overflow-hidden flex flex-col bg-neutral-900 border-neutral-700 p-0">
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Header Area */}
|
||||
<div className="flex items-start justify-between p-6 border-b border-neutral-800">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white flex items-center gap-2">
|
||||
<Wand className="h-5 w-5 text-emerald-400" />
|
||||
{initialTool ? "Edit Custom Tool" : "Create Custom Tool"}
|
||||
</h2>
|
||||
<p className="text-neutral-400 text-sm mt-1">
|
||||
Configure an HTTP tool for your agent to interact with external APIs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="outline" className="bg-neutral-800 text-emerald-400 border-emerald-500/30 uppercase text-xs font-semibold px-2 py-0.5">
|
||||
{tool.method || "GET"}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Left Side - Navigation */}
|
||||
<div className="w-[200px] bg-neutral-800/50 border-r border-neutral-800 flex-shrink-0">
|
||||
<div className="py-4">
|
||||
<nav className="space-y-1 px-2">
|
||||
{[
|
||||
{ id: 'basics', label: 'Basic Info', icon: <Info className="h-4 w-4" /> },
|
||||
{ id: 'endpoint', label: 'Endpoint', icon: <Globe className="h-4 w-4" /> },
|
||||
{ id: 'headers', label: 'Headers', icon: <Server className="h-4 w-4" /> },
|
||||
{ id: 'body', label: 'Body Params', icon: <FileJson className="h-4 w-4" /> },
|
||||
{ id: 'path', label: 'Path Params', icon: <Code className="h-4 w-4" /> },
|
||||
{ id: 'query', label: 'Query Params', icon: <LayoutList className="h-4 w-4" /> },
|
||||
{ id: 'defaults', label: 'Default Values', icon: <Database className="h-4 w-4" /> },
|
||||
{ id: 'error', label: 'Error Handling', icon: <Settings className="h-4 w-4" /> },
|
||||
].map(item => (
|
||||
<button
|
||||
key={item.id}
|
||||
className={cn(
|
||||
"w-full flex items-center gap-2 px-3 py-2 text-sm rounded-md transition-colors",
|
||||
activeTab === item.id
|
||||
? "bg-emerald-500/10 text-emerald-400"
|
||||
: "text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800"
|
||||
)}
|
||||
onClick={() => setActiveTab(item.id)}
|
||||
>
|
||||
{item.icon}
|
||||
<span>{item.label}</span>
|
||||
{(
|
||||
(item.id === 'headers' && headersList.length > 0) ||
|
||||
(item.id === 'body' && bodyParams.length > 0) ||
|
||||
(item.id === 'path' && pathParams.length > 0) ||
|
||||
(item.id === 'query' && queryParams.length > 0) ||
|
||||
(item.id === 'defaults' && valuesList.length > 0)
|
||||
) && (
|
||||
<span className="ml-auto bg-emerald-500/20 text-emerald-400 text-xs rounded-full px-1.5 py-0.5 min-w-[18px]">
|
||||
{item.id === 'headers' && headersList.length}
|
||||
{item.id === 'body' && bodyParams.length}
|
||||
{item.id === 'path' && pathParams.length}
|
||||
{item.id === 'query' && queryParams.length}
|
||||
{item.id === 'defaults' && valuesList.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side - Content */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{activeTab === 'basics' && (
|
||||
<div className="p-6">
|
||||
<div className="space-y-6">
|
||||
<ParamField
|
||||
label="Tool Name"
|
||||
tooltip="A unique identifier for this tool. Will be used by the agent to reference this tool."
|
||||
>
|
||||
<Input
|
||||
value={tool.name || ""}
|
||||
onChange={(e) => setTool({ ...tool, name: e.target.value })}
|
||||
onBlur={(e) => {
|
||||
const sanitizedName = sanitizeAgentName(e.target.value);
|
||||
if (sanitizedName !== e.target.value) {
|
||||
setTool({ ...tool, name: sanitizedName });
|
||||
}
|
||||
}}
|
||||
className="bg-neutral-800 border-neutral-700 text-white"
|
||||
placeholder="e.g. weatherApi, searchTool"
|
||||
/>
|
||||
</ParamField>
|
||||
|
||||
<ParamField
|
||||
label="Description"
|
||||
tooltip="A clear description of what this tool does. The agent will use this to determine when to use the tool."
|
||||
>
|
||||
<Input
|
||||
value={tool.description || ""}
|
||||
onChange={(e) => setTool({ ...tool, description: e.target.value })}
|
||||
className="bg-neutral-800 border-neutral-700 text-white"
|
||||
placeholder="Provides weather information for a given location"
|
||||
/>
|
||||
</ParamField>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'endpoint' && (
|
||||
<div className="p-6">
|
||||
<div className="space-y-6">
|
||||
<ParamField
|
||||
label="HTTP Method"
|
||||
tooltip="The HTTP method to use for the request."
|
||||
>
|
||||
<div className="flex gap-1">
|
||||
{['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].map(method => (
|
||||
<Button
|
||||
key={method}
|
||||
type="button"
|
||||
onClick={() => setTool({ ...tool, method })}
|
||||
className={cn(
|
||||
"px-4 py-2 rounded font-medium text-sm flex-1",
|
||||
tool.method === method
|
||||
? "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30"
|
||||
: "bg-neutral-800 text-neutral-400 border border-neutral-700 hover:bg-neutral-700 hover:text-neutral-200"
|
||||
)}
|
||||
>
|
||||
{method}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</ParamField>
|
||||
|
||||
<ParamField
|
||||
label="Endpoint URL"
|
||||
tooltip="The complete URL to call. Can include path parameters with the format {paramName}."
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
value={tool.endpoint || ""}
|
||||
onChange={(e) => setTool({ ...tool, endpoint: e.target.value })}
|
||||
className="bg-neutral-800 border-neutral-700 text-white font-mono"
|
||||
placeholder="https://api.example.com/v1/resource/{id}"
|
||||
/>
|
||||
{tool.endpoint && tool.endpoint.includes('{') && (
|
||||
<p className="text-xs text-amber-400">
|
||||
<Info className="h-3 w-3 inline-block mr-1" />
|
||||
URL contains path variables. Don't forget to define them in the Path Parameters section.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</ParamField>
|
||||
|
||||
<div className="p-4 bg-neutral-800/80 border border-emerald-600/20 rounded-md mt-6">
|
||||
<h3 className="text-emerald-400 text-sm font-medium mb-2 flex items-center">
|
||||
<Info className="h-4 w-4 mr-2" /> How to use variables in your endpoint
|
||||
</h3>
|
||||
<p className="text-neutral-300 text-sm mb-3">
|
||||
You can use dynamic variables in your endpoint using the <code className="bg-neutral-700 px-1.5 py-0.5 rounded text-emerald-300">{"{VARIABLE_NAME}"}</code> syntax.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 gap-4 text-xs">
|
||||
<div>
|
||||
<h4 className="font-medium text-white mb-1">Example:</h4>
|
||||
<code className="block bg-neutral-900 text-neutral-200 p-2 rounded">
|
||||
https://api.example.com/users/<span className="text-emerald-400">{"{userId}"}</span>/profile
|
||||
</code>
|
||||
<p className="mt-1 text-neutral-400">Define <code className="bg-neutral-700 px-1 py-0.5 rounded text-emerald-300">userId</code> as a Path Parameter</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'headers' && (
|
||||
<div className="p-6">
|
||||
<ParamField
|
||||
label="HTTP Headers"
|
||||
tooltip="Headers to send with each request. Common examples include Authorization, Content-Type, Accept, etc."
|
||||
>
|
||||
<FieldList
|
||||
items={headersList}
|
||||
onAdd={handleAddHeader}
|
||||
onRemove={handleRemoveHeader}
|
||||
onChange={(id, field, value) => {
|
||||
handleHeaderChange(
|
||||
id,
|
||||
field as "key" | "value",
|
||||
value as string
|
||||
);
|
||||
}}
|
||||
fields={[
|
||||
{ name: "Header Name", field: "key", placeholder: "e.g. Authorization", width: 6 },
|
||||
{ name: "Header Value", field: "value", placeholder: "e.g. Bearer token123", width: 6 }
|
||||
]}
|
||||
addText="Add Header"
|
||||
emptyText="No headers configured. Add headers to customize your HTTP requests."
|
||||
icon={<Server className="h-10 w-10 text-neutral-700" />}
|
||||
/>
|
||||
</ParamField>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'body' && (
|
||||
<div className="p-6">
|
||||
<div className="mb-6 p-4 bg-neutral-800/80 border border-emerald-600/20 rounded-md">
|
||||
<h3 className="text-emerald-400 text-sm font-medium mb-2 flex items-center">
|
||||
<Info className="h-4 w-4 mr-2" /> About Body Parameters
|
||||
</h3>
|
||||
<p className="text-neutral-300 text-sm">
|
||||
Parameters that will be sent in the request body. Only applicable for POST, PUT, and PATCH methods.
|
||||
You can use variables like <code className="bg-neutral-700 px-1.5 py-0.5 rounded text-emerald-300">{"{variableName}"}</code> that will be replaced at runtime.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FieldList
|
||||
items={bodyParams}
|
||||
onAdd={handleAddBodyParam}
|
||||
onRemove={handleRemoveBodyParam}
|
||||
onChange={(id, field, value) => {
|
||||
if (field === "key") {
|
||||
handleBodyParamChange(id, "key", value as string);
|
||||
} else if (field.startsWith("param.")) {
|
||||
const paramField = field.split('.')[1] as keyof HTTPToolParameter;
|
||||
handleBodyParamChange(id, paramField,
|
||||
paramField === "required" ? value as boolean : value as string
|
||||
);
|
||||
}
|
||||
}}
|
||||
fields={[
|
||||
{ name: "Parameter Name", field: "key", placeholder: "e.g. userId", width: 4 },
|
||||
{ name: "Type", field: "param.type", placeholder: "", width: 2, type: "select" },
|
||||
{ name: "Description", field: "param.description", placeholder: "What this parameter does", width: 4 },
|
||||
{ name: "Required", field: "param.required", placeholder: "", width: 2, type: "checkbox" }
|
||||
]}
|
||||
addText="Add Body Parameter"
|
||||
emptyText="No body parameters configured. Add parameters for POST/PUT/PATCH requests."
|
||||
icon={<FileJson className="h-10 w-10 text-neutral-700" />}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'path' && (
|
||||
<div className="p-6">
|
||||
<div className="mb-6 p-4 bg-neutral-800/80 border border-emerald-600/20 rounded-md">
|
||||
<h3 className="text-emerald-400 text-sm font-medium mb-2 flex items-center">
|
||||
<Info className="h-4 w-4 mr-2" /> About Path Parameters
|
||||
</h3>
|
||||
<p className="text-neutral-300 text-sm">
|
||||
Path parameters are placeholders in your URL path that will be replaced with actual values.
|
||||
For example, in <code className="bg-neutral-700 px-1.5 py-0.5 rounded text-emerald-300">https://api.example.com/users/{"{userId}"}</code>,
|
||||
<code className="bg-neutral-700 px-1 py-0.5 rounded text-emerald-300">userId</code> is a path parameter.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FieldList
|
||||
items={pathParams}
|
||||
onAdd={handleAddPathParam}
|
||||
onRemove={handleRemovePathParam}
|
||||
onChange={(id, field, value) => {
|
||||
handlePathParamChange(
|
||||
id,
|
||||
field as "key" | "desc",
|
||||
value as string
|
||||
);
|
||||
}}
|
||||
fields={[
|
||||
{ name: "Parameter Name", field: "key", placeholder: "e.g. userId", width: 4 },
|
||||
{ name: "Description", field: "desc", placeholder: "What this parameter represents", width: 6 },
|
||||
{ name: "Required", field: "required", placeholder: "", width: 2, type: "checkbox" }
|
||||
]}
|
||||
addText="Add Path Parameter"
|
||||
emptyText="No path parameters configured. Add parameters if your endpoint URL contains {placeholders}."
|
||||
icon={<Code className="h-10 w-10 text-neutral-700" />}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'query' && (
|
||||
<div className="p-6">
|
||||
<div className="mb-6 p-4 bg-neutral-800/80 border border-emerald-600/20 rounded-md">
|
||||
<h3 className="text-emerald-400 text-sm font-medium mb-2 flex items-center">
|
||||
<Info className="h-4 w-4 mr-2" /> About Query Parameters
|
||||
</h3>
|
||||
<p className="text-neutral-300 text-sm">
|
||||
Query parameters are added to the URL after a question mark (?) to filter or customize the request.
|
||||
For example: <code className="bg-neutral-700 px-1.5 py-0.5 rounded text-emerald-300">https://api.example.com/search?query=term&limit=10</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FieldList
|
||||
items={queryParams}
|
||||
onAdd={handleAddQueryParam}
|
||||
onRemove={handleRemoveQueryParam}
|
||||
onChange={(id, field, value) => {
|
||||
handleQueryParamChange(
|
||||
id,
|
||||
field as "key" | "value",
|
||||
value as string
|
||||
);
|
||||
}}
|
||||
fields={[
|
||||
{ name: "Parameter Name", field: "key", placeholder: "e.g. search", width: 4 },
|
||||
{ name: "Description", field: "value", placeholder: "What this parameter does", width: 6 },
|
||||
{ name: "Required", field: "required", placeholder: "", width: 2, type: "checkbox" }
|
||||
]}
|
||||
addText="Add Query Parameter"
|
||||
emptyText="No query parameters configured. Add parameters to customize your URL query string."
|
||||
icon={<LayoutList className="h-10 w-10 text-neutral-700" />}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'defaults' && (
|
||||
<div className="p-6">
|
||||
<div className="mb-6 p-4 bg-neutral-800/80 border border-emerald-600/20 rounded-md">
|
||||
<h3 className="text-emerald-400 text-sm font-medium mb-2 flex items-center">
|
||||
<Info className="h-4 w-4 mr-2" /> About Default Values
|
||||
</h3>
|
||||
<p className="text-neutral-300 text-sm">
|
||||
Set default values for parameters you've defined in the Body, Path, or Query sections.
|
||||
These values will be used when the parameter isn't explicitly provided during a tool call.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FieldList
|
||||
items={valuesList}
|
||||
onAdd={handleAddValue}
|
||||
onRemove={handleRemoveValue}
|
||||
onChange={(id, field, value) => {
|
||||
handleValueChange(
|
||||
id,
|
||||
field as "key" | "value",
|
||||
value as string
|
||||
);
|
||||
}}
|
||||
fields={[
|
||||
{ name: "Parameter Name", field: "key", placeholder: "Name of an existing parameter", width: 5 },
|
||||
{ name: "Default Value", field: "value", placeholder: "Value to use if not provided", width: 7 }
|
||||
]}
|
||||
addText="Add Default Value"
|
||||
emptyText="No default values configured. Add default values for your previously defined parameters."
|
||||
icon={<Database className="h-10 w-10 text-neutral-700" />}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'error' && (
|
||||
<div className="p-6">
|
||||
<div className="mb-6 p-4 bg-neutral-800/80 border border-emerald-600/20 rounded-md">
|
||||
<h3 className="text-emerald-400 text-sm font-medium mb-2 flex items-center">
|
||||
<Info className="h-4 w-4 mr-2" /> About Error Handling
|
||||
</h3>
|
||||
<p className="text-neutral-300 text-sm">
|
||||
Configure how the tool should handle errors and timeouts when making HTTP requests.
|
||||
Proper error handling ensures reliable operation of your agent.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<ParamField
|
||||
label="Timeout (seconds)"
|
||||
tooltip="Maximum time to wait for a response before considering the request failed."
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={timeout}
|
||||
onChange={(e) => setTimeout(Number(e.target.value))}
|
||||
className="w-32 bg-neutral-800 border-neutral-700 text-white"
|
||||
/>
|
||||
</ParamField>
|
||||
|
||||
<ParamField
|
||||
label="Fallback Error Code"
|
||||
tooltip="Error code to return if the request fails."
|
||||
>
|
||||
<Input
|
||||
value={fallbackError}
|
||||
onChange={(e) => setFallbackError(e.target.value)}
|
||||
className="bg-neutral-800 border-neutral-700 text-white"
|
||||
placeholder="e.g. API_ERROR"
|
||||
/>
|
||||
</ParamField>
|
||||
|
||||
<ParamField
|
||||
label="Fallback Error Message"
|
||||
tooltip="Human-readable message to return if the request fails."
|
||||
>
|
||||
<Input
|
||||
value={fallbackMessage}
|
||||
onChange={(e) => setFallbackMessage(e.target.value)}
|
||||
className="bg-neutral-800 border-neutral-700 text-white"
|
||||
placeholder="e.g. Failed to retrieve data from the API."
|
||||
/>
|
||||
</ParamField>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Area */}
|
||||
<div className="border-t border-neutral-800 p-4 flex items-center justify-between">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="text-neutral-400 hover:text-neutral-100 hover:bg-neutral-800"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="bg-emerald-500 text-neutral-950 hover:bg-emerald-400"
|
||||
disabled={!tool.name || !tool.endpoint}
|
||||
>
|
||||
{initialTool ? "Save Changes" : "Create Tool"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
158
frontend/app/agents/dialogs/FolderDialog.tsx
Normal file
158
frontend/app/agents/dialogs/FolderDialog.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/dialogs/FolderDialog.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 { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface Folder {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface FolderDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSave: (folder: { name: string; description: string }) => Promise<void>;
|
||||
editingFolder: Folder | null;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function FolderDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSave,
|
||||
editingFolder,
|
||||
isLoading = false,
|
||||
}: FolderDialogProps) {
|
||||
const [folder, setFolder] = useState<{
|
||||
name: string;
|
||||
description: string;
|
||||
}>({
|
||||
name: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (editingFolder) {
|
||||
setFolder({
|
||||
name: editingFolder.name,
|
||||
description: editingFolder.description,
|
||||
});
|
||||
} else {
|
||||
setFolder({
|
||||
name: "",
|
||||
description: "",
|
||||
});
|
||||
}
|
||||
}, [editingFolder, open]);
|
||||
|
||||
const handleSave = async () => {
|
||||
await onSave(folder);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[500px] bg-[#1a1a1a] border-[#333] text-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingFolder ? "Edit Folder" : "New Folder"}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
{editingFolder
|
||||
? "Update the existing folder information"
|
||||
: "Fill in the information to create a new folder"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="folder-name" className="text-neutral-300">
|
||||
Folder Name
|
||||
</Label>
|
||||
<Input
|
||||
id="folder-name"
|
||||
value={folder.name}
|
||||
onChange={(e) =>
|
||||
setFolder({ ...folder, name: e.target.value })
|
||||
}
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="folder-description" className="text-neutral-300">
|
||||
Description (optional)
|
||||
</Label>
|
||||
<Textarea
|
||||
id="folder-description"
|
||||
value={folder.description}
|
||||
onChange={(e) =>
|
||||
setFolder({ ...folder, description: e.target.value })
|
||||
}
|
||||
className="bg-[#222] border-[#444] text-white resize-none h-20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
disabled={!folder.name || isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="animate-spin h-4 w-4 border-2 border-black border-t-transparent rounded-full mr-1"></div>
|
||||
) : null}
|
||||
{editingFolder ? "Save Changes" : "Create Folder"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
239
frontend/app/agents/dialogs/ImportAgentDialog.tsx
Normal file
239
frontend/app/agents/dialogs/ImportAgentDialog.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/dialogs/ImportAgentDialog.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 15, 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 { useState, useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Loader2, Upload, FileJson } from "lucide-react";
|
||||
import { importAgentFromJson } from "@/services/agentService";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
interface ImportAgentDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSuccess: () => void;
|
||||
clientId: string;
|
||||
folderId?: string | null;
|
||||
}
|
||||
|
||||
export function ImportAgentDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSuccess,
|
||||
clientId,
|
||||
folderId,
|
||||
}: ImportAgentDialogProps) {
|
||||
const { toast } = useToast();
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const selectedFile = e.target.files?.[0];
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragActive(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragActive(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragActive(false);
|
||||
|
||||
const droppedFile = e.dataTransfer.files?.[0];
|
||||
if (droppedFile && droppedFile.type === "application/json") {
|
||||
setFile(droppedFile);
|
||||
} else {
|
||||
toast({
|
||||
title: "Invalid file",
|
||||
description: "Please upload a JSON file",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectFile = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!file || !clientId) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
await importAgentFromJson(file, clientId, folderId);
|
||||
|
||||
toast({
|
||||
title: "Import successful",
|
||||
description: folderId
|
||||
? "Agent was imported successfully and added to the current folder"
|
||||
: "Agent was imported successfully",
|
||||
});
|
||||
|
||||
// Call the success callback to refresh the agent list
|
||||
onSuccess();
|
||||
|
||||
// Close the dialog
|
||||
onOpenChange(false);
|
||||
|
||||
// Reset state
|
||||
setFile(null);
|
||||
} catch (error) {
|
||||
console.error("Error importing agent:", error);
|
||||
toast({
|
||||
title: "Import failed",
|
||||
description: "There was an error importing the agent",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
if (!isLoading) {
|
||||
setFile(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(newOpen) => {
|
||||
if (!newOpen) {
|
||||
resetState();
|
||||
}
|
||||
onOpenChange(newOpen);
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-[500px] bg-[#1a1a1a] border-[#333] text-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Import Agent</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
Upload a JSON file to import an agent
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col space-y-4 py-4">
|
||||
<div
|
||||
className={`h-40 border-2 border-dashed rounded-md flex flex-col items-center justify-center p-4 transition-colors cursor-pointer ${
|
||||
dragActive
|
||||
? "border-emerald-400 bg-emerald-900/20"
|
||||
: file
|
||||
? "border-emerald-600 bg-emerald-900/10"
|
||||
: "border-[#444] hover:border-emerald-600/50"
|
||||
}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={handleSelectFile}
|
||||
>
|
||||
{file ? (
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<FileJson className="h-10 w-10 text-emerald-400" />
|
||||
<span className="text-emerald-400 font-medium">{file.name}</span>
|
||||
<span className="text-neutral-400 text-xs">
|
||||
{Math.round(file.size / 1024)} KB
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<Upload className="h-10 w-10 text-neutral-500" />
|
||||
<p className="text-neutral-400 text-center">
|
||||
Drag & drop your JSON file here or click to browse
|
||||
</p>
|
||||
<span className="text-neutral-500 text-xs">
|
||||
Only JSON files are accepted
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
accept=".json,application/json"
|
||||
style={{ display: "none" }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
disabled={isLoading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
disabled={!file || isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Importing...
|
||||
</>
|
||||
) : (
|
||||
"Import Agent"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
278
frontend/app/agents/dialogs/MCPDialog.tsx
Normal file
278
frontend/app/agents/dialogs/MCPDialog.tsx
Normal file
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/dialogs/MCPDialog.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 { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { MCPServer } from "@/types/mcpServer";
|
||||
import { Server } from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface MCPDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSave: (mcpConfig: {
|
||||
id: string;
|
||||
envs: Record<string, string>;
|
||||
tools: string[];
|
||||
}) => void;
|
||||
availableMCPs: MCPServer[];
|
||||
selectedMCP?: MCPServer | null;
|
||||
initialEnvs?: Record<string, string>;
|
||||
initialTools?: string[];
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
export function MCPDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSave,
|
||||
availableMCPs,
|
||||
selectedMCP: initialSelectedMCP = null,
|
||||
initialEnvs = {},
|
||||
initialTools = [],
|
||||
clientId,
|
||||
}: MCPDialogProps) {
|
||||
const [selectedMCP, setSelectedMCP] = useState<MCPServer | null>(null);
|
||||
const [mcpEnvs, setMcpEnvs] = useState<Record<string, string>>({});
|
||||
const [selectedMCPTools, setSelectedMCPTools] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (initialSelectedMCP) {
|
||||
setSelectedMCP(initialSelectedMCP);
|
||||
setMcpEnvs(initialEnvs);
|
||||
setSelectedMCPTools(initialTools);
|
||||
} else {
|
||||
setSelectedMCP(null);
|
||||
setMcpEnvs({});
|
||||
setSelectedMCPTools([]);
|
||||
}
|
||||
}
|
||||
}, [open, initialSelectedMCP, initialEnvs, initialTools]);
|
||||
|
||||
const handleSelectMCP = (value: string) => {
|
||||
const mcp = availableMCPs.find((m) => m.id === value);
|
||||
if (mcp) {
|
||||
setSelectedMCP(mcp);
|
||||
const initialEnvs: Record<string, string> = {};
|
||||
Object.keys(mcp.environments || {}).forEach((key) => {
|
||||
initialEnvs[key] = "";
|
||||
});
|
||||
setMcpEnvs(initialEnvs);
|
||||
setSelectedMCPTools([]);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMCPTool = (tool: string) => {
|
||||
if (selectedMCPTools.includes(tool)) {
|
||||
setSelectedMCPTools(selectedMCPTools.filter((t) => t !== tool));
|
||||
} else {
|
||||
setSelectedMCPTools([...selectedMCPTools, tool]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!selectedMCP) return;
|
||||
|
||||
onSave({
|
||||
id: selectedMCP.id,
|
||||
envs: mcpEnvs,
|
||||
tools: selectedMCPTools,
|
||||
});
|
||||
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-hidden flex flex-col bg-[#1a1a1a] border-[#333]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">
|
||||
Configure MCP Server
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
Select a MCP server and configure its tools.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="mcp-select" className="text-neutral-300">
|
||||
MCP Server
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedMCP?.id}
|
||||
onValueChange={handleSelectMCP}
|
||||
>
|
||||
<SelectTrigger className="bg-[#222] border-[#444] text-white">
|
||||
<SelectValue placeholder="Select a MCP server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#222] border-[#444] text-white">
|
||||
{availableMCPs.map((mcp) => (
|
||||
<SelectItem
|
||||
key={mcp.id}
|
||||
value={mcp.id}
|
||||
className="data-[selected]:bg-[#333] data-[highlighted]:bg-[#333] !text-white focus:!text-white hover:text-emerald-400 data-[selected]:!text-emerald-400"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Server className="h-4 w-4 text-emerald-400" />
|
||||
{mcp.name}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{selectedMCP && (
|
||||
<>
|
||||
<div className="border border-[#444] rounded-md p-3 bg-[#222]">
|
||||
<p className="font-medium text-white">{selectedMCP.name}</p>
|
||||
<p className="text-sm text-neutral-400">
|
||||
{selectedMCP.description?.substring(0, 100)}...
|
||||
</p>
|
||||
<div className="mt-2 text-xs text-neutral-400">
|
||||
<p>
|
||||
<strong>Type:</strong> {selectedMCP.type}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Configuration:</strong>{" "}
|
||||
{selectedMCP.config_type === "sse" ? "SSE" : "Studio"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedMCP.environments &&
|
||||
Object.keys(selectedMCP.environments).length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-white">
|
||||
Environment Variables
|
||||
</h3>
|
||||
{Object.entries(selectedMCP.environments || {}).map(
|
||||
([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="grid grid-cols-3 items-center gap-4"
|
||||
>
|
||||
<Label
|
||||
htmlFor={`env-${key}`}
|
||||
className="text-right text-neutral-300"
|
||||
>
|
||||
{key}
|
||||
</Label>
|
||||
<Input
|
||||
id={`env-${key}`}
|
||||
value={mcpEnvs[key] || ""}
|
||||
onChange={(e) =>
|
||||
setMcpEnvs({
|
||||
...mcpEnvs,
|
||||
[key]: e.target.value,
|
||||
})
|
||||
}
|
||||
className="col-span-2 bg-[#222] border-[#444] text-white"
|
||||
placeholder={value as string}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedMCP.tools && selectedMCP.tools.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-white">
|
||||
Available Tools
|
||||
</h3>
|
||||
<div className="border border-[#444] rounded-md p-3 bg-[#222]">
|
||||
{selectedMCP.tools.map((tool: any) => (
|
||||
<div
|
||||
key={tool.id}
|
||||
className="flex items-center space-x-2 py-1"
|
||||
>
|
||||
<Checkbox
|
||||
id={`tool-${tool.id}`}
|
||||
checked={selectedMCPTools.includes(tool.id)}
|
||||
onCheckedChange={() => toggleMCPTool(tool.id)}
|
||||
className="data-[state=checked]:bg-emerald-400 data-[state=checked]:border-emerald-400"
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`tool-${tool.id}`}
|
||||
className="text-sm text-neutral-300"
|
||||
>
|
||||
{tool.name}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="p-4 pt-2 border-t border-[#333]">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
disabled={!selectedMCP}
|
||||
>
|
||||
Add MCP
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
136
frontend/app/agents/dialogs/MoveAgentDialog.tsx
Normal file
136
frontend/app/agents/dialogs/MoveAgentDialog.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/dialogs/MoveAgentDialog.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 { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { Folder, Home } from "lucide-react";
|
||||
|
||||
interface Folder {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface MoveAgentDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
agent: Agent | null;
|
||||
folders: Folder[];
|
||||
onMove: (folderId: string | null) => Promise<void>;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function MoveAgentDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
agent,
|
||||
folders,
|
||||
onMove,
|
||||
isLoading = false,
|
||||
}: MoveAgentDialogProps) {
|
||||
const handleMove = async (folderId: string | null) => {
|
||||
await onMove(folderId);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[500px] bg-[#1a1a1a] border-[#333] text-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Move Agent</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
Choose a folder to move the agent "{agent?.name}"
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4">
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
className="w-full text-left px-4 py-3 rounded-md flex items-center bg-[#222] border border-[#444] hover:bg-[#333] hover:border-emerald-400/50 transition-colors"
|
||||
onClick={() => handleMove(null)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Home className="h-5 w-5 mr-3 text-neutral-400" />
|
||||
<div>
|
||||
<div className="font-medium">Remove from folder</div>
|
||||
<p className="text-sm text-neutral-400">
|
||||
The agent will be visible in "All agents"
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{folders.map((folder) => (
|
||||
<button
|
||||
key={folder.id}
|
||||
className="w-full text-left px-4 py-3 rounded-md flex items-center bg-[#222] border border-[#444] hover:bg-[#333] hover:border-emerald-400/50 transition-colors"
|
||||
onClick={() => handleMove(folder.id)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Folder className="h-5 w-5 mr-3 text-emerald-400" />
|
||||
<div>
|
||||
<div className="font-medium">{folder.name}</div>
|
||||
{folder.description && (
|
||||
<p className="text-sm text-neutral-400 truncate">
|
||||
{folder.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{isLoading && (
|
||||
<div className="flex items-center">
|
||||
<div className="animate-spin h-4 w-4 border-2 border-emerald-400 border-t-transparent rounded-full mr-2"></div>
|
||||
<span className="text-neutral-400">Moving...</span>
|
||||
</div>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
172
frontend/app/agents/dialogs/ShareAgentDialog.tsx
Normal file
172
frontend/app/agents/dialogs/ShareAgentDialog.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/dialogs/ShareAgentDialog.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 { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Copy, Share2, ExternalLink } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
interface ShareAgentDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
agent: Agent;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export function ShareAgentDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
agent,
|
||||
apiKey,
|
||||
}: ShareAgentDialogProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [shareLink, setShareLink] = useState("");
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
if (open && agent && apiKey) {
|
||||
const baseUrl = window.location.origin;
|
||||
setShareLink(`${baseUrl}/shared-chat?agent=${agent.id}&key=${apiKey}`);
|
||||
}
|
||||
}, [open, agent, apiKey]);
|
||||
|
||||
const handleCopyLink = () => {
|
||||
if (shareLink) {
|
||||
navigator.clipboard.writeText(shareLink);
|
||||
setCopied(true);
|
||||
toast({
|
||||
title: "Link copied!",
|
||||
description: "The share link has been copied to the clipboard.",
|
||||
});
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyApiKey = () => {
|
||||
if (apiKey) {
|
||||
navigator.clipboard.writeText(apiKey);
|
||||
setCopied(true);
|
||||
toast({
|
||||
title: "API Key copied!",
|
||||
description: "The API Key has been copied to the clipboard.",
|
||||
});
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[500px] bg-[#1a1a1a] border-[#333] text-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Share2 className="h-5 w-5 text-emerald-400" />
|
||||
Share Agent
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
Share this agent with others without the need to login.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium text-white">Share Link</h3>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
readOnly
|
||||
value={shareLink}
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="shrink-0 bg-[#222] border-[#444] hover:bg-[#333] text-emerald-400 hover:text-emerald-300"
|
||||
onClick={handleCopyLink}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-400">
|
||||
Any person with this link can access the agent using the included API key.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium text-white">API Key</h3>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
readOnly
|
||||
value={apiKey}
|
||||
type="password"
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="shrink-0 bg-[#222] border-[#444] hover:bg-[#333] text-emerald-400 hover:text-emerald-300"
|
||||
onClick={handleCopyApiKey}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-400">
|
||||
The API key allows access to the agent. Do not share with untrusted people.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="border-t border-[#333] pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => window.open(shareLink, "_blank")}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white flex gap-2"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
Open Link
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
331
frontend/app/agents/forms/AgentForm.tsx
Normal file
331
frontend/app/agents/forms/AgentForm.tsx
Normal file
@@ -0,0 +1,331 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/forms/AgentForm.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 { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { ApiKey } from "@/services/agentService";
|
||||
import { MCPServer } from "@/types/mcpServer";
|
||||
import { useState, useEffect } from "react";
|
||||
import { BasicInfoTab } from "./BasicInfoTab";
|
||||
import { ConfigurationTab } from "./ConfigurationTab";
|
||||
import { SubAgentsTab } from "./SubAgentsTab";
|
||||
import { MCPDialog } from "../dialogs/MCPDialog";
|
||||
import { CustomMCPDialog } from "../dialogs/CustomMCPDialog";
|
||||
|
||||
interface ModelOption {
|
||||
value: string;
|
||||
label: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
interface AgentFormProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
initialValues: Partial<Agent>;
|
||||
apiKeys: ApiKey[];
|
||||
availableModels: ModelOption[];
|
||||
availableMCPs: MCPServer[];
|
||||
agents: Agent[];
|
||||
onOpenApiKeysDialog: () => void;
|
||||
onOpenMCPDialog: (mcp?: any) => void;
|
||||
onOpenCustomMCPDialog: (customMCP?: any) => void;
|
||||
onSave: (values: Partial<Agent>) => Promise<void>;
|
||||
isLoading?: boolean;
|
||||
getAgentNameById: (id: string) => string;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
export function AgentForm({
|
||||
open,
|
||||
onOpenChange,
|
||||
initialValues,
|
||||
apiKeys,
|
||||
availableModels,
|
||||
availableMCPs,
|
||||
agents,
|
||||
onOpenApiKeysDialog,
|
||||
onOpenMCPDialog: externalOnOpenMCPDialog,
|
||||
onOpenCustomMCPDialog: externalOnOpenCustomMCPDialog,
|
||||
onSave,
|
||||
isLoading = false,
|
||||
getAgentNameById,
|
||||
clientId,
|
||||
}: AgentFormProps) {
|
||||
const [values, setValues] = useState<Partial<Agent>>(initialValues);
|
||||
const [activeTab, setActiveTab] = useState("basic");
|
||||
|
||||
const [mcpDialogOpen, setMcpDialogOpen] = useState(false);
|
||||
const [selectedMCP, setSelectedMCP] = useState<any>(null);
|
||||
const [customMcpDialogOpen, setCustomMcpDialogOpen] = useState(false);
|
||||
const [selectedCustomMCP, setSelectedCustomMCP] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setValues(initialValues);
|
||||
setActiveTab("basic");
|
||||
}
|
||||
}, [open, initialValues]);
|
||||
|
||||
const handleOpenMCPDialog = (mcpConfig: any = null) => {
|
||||
setSelectedMCP(mcpConfig);
|
||||
setMcpDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleOpenCustomMCPDialog = (customMCP: any = null) => {
|
||||
setSelectedCustomMCP(customMCP);
|
||||
setCustomMcpDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleConfigureMCP = (mcpConfig: any) => {
|
||||
handleOpenMCPDialog(mcpConfig);
|
||||
};
|
||||
|
||||
const handleRemoveMCP = (mcpId: string) => {
|
||||
setValues({
|
||||
...values,
|
||||
config: {
|
||||
...values.config,
|
||||
mcp_servers:
|
||||
values.config?.mcp_servers?.filter((mcp) => mcp.id !== mcpId) || [],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfigureCustomMCP = (customMCP: any) => {
|
||||
handleOpenCustomMCPDialog(customMCP);
|
||||
};
|
||||
|
||||
const handleRemoveCustomMCP = (url: string) => {
|
||||
setValues({
|
||||
...values,
|
||||
config: {
|
||||
...values.config,
|
||||
custom_mcp_servers:
|
||||
values.config?.custom_mcp_servers?.filter(
|
||||
(customMCP) => customMCP.url !== url
|
||||
) || [],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveMCP = (mcpConfig: any) => {
|
||||
const updatedMcpServers = [...(values.config?.mcp_servers || [])];
|
||||
const existingIndex = updatedMcpServers.findIndex(
|
||||
(mcp) => mcp.id === mcpConfig.id
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
updatedMcpServers[existingIndex] = mcpConfig;
|
||||
} else {
|
||||
updatedMcpServers.push(mcpConfig);
|
||||
}
|
||||
|
||||
setValues({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
mcp_servers: updatedMcpServers,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveCustomMCP = (customMCP: any) => {
|
||||
const updatedCustomMCPs = [...(values.config?.custom_mcp_servers || [])];
|
||||
const existingIndex = updatedCustomMCPs.findIndex(
|
||||
(mcp) => mcp.url === customMCP.url
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
updatedCustomMCPs[existingIndex] = customMCP;
|
||||
} else {
|
||||
updatedCustomMCPs.push(customMCP);
|
||||
}
|
||||
|
||||
setValues({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
custom_mcp_servers: updatedCustomMCPs,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const finalValues = {
|
||||
...values,
|
||||
client_id: clientId,
|
||||
name: values.name,
|
||||
};
|
||||
|
||||
await onSave(finalValues);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-hidden flex flex-col bg-[#1a1a1a] border-[#333]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">
|
||||
{initialValues.id ? "Edit Agent" : "New Agent"}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
{initialValues.id
|
||||
? "Edit the existing agent information"
|
||||
: "Fill in the information to create a new agent"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="flex-1 overflow-hidden flex flex-col"
|
||||
>
|
||||
<TabsList className="grid grid-cols-3 bg-[#222]">
|
||||
<TabsTrigger
|
||||
value="basic"
|
||||
className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400"
|
||||
>
|
||||
Basic Information
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="config"
|
||||
className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400"
|
||||
>
|
||||
Configuration
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="subagents"
|
||||
className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400"
|
||||
>
|
||||
Sub-Agents
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<ScrollArea className="flex-1 overflow-auto">
|
||||
<TabsContent value="basic" className="p-4 space-y-4">
|
||||
<BasicInfoTab
|
||||
values={values}
|
||||
onChange={setValues}
|
||||
apiKeys={apiKeys}
|
||||
availableModels={availableModels}
|
||||
onOpenApiKeysDialog={onOpenApiKeysDialog}
|
||||
clientId={clientId}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="config" className="p-4 space-y-4">
|
||||
<ConfigurationTab
|
||||
values={values}
|
||||
onChange={setValues}
|
||||
agents={agents}
|
||||
availableMCPs={availableMCPs}
|
||||
apiKeys={apiKeys}
|
||||
availableModels={availableModels}
|
||||
getAgentNameById={getAgentNameById}
|
||||
onOpenApiKeysDialog={onOpenApiKeysDialog}
|
||||
onConfigureMCP={handleConfigureMCP}
|
||||
onRemoveMCP={handleRemoveMCP}
|
||||
onConfigureCustomMCP={handleConfigureCustomMCP}
|
||||
onRemoveCustomMCP={handleRemoveCustomMCP}
|
||||
onOpenMCPDialog={handleOpenMCPDialog}
|
||||
onOpenCustomMCPDialog={handleOpenCustomMCPDialog}
|
||||
clientId={clientId}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="subagents" className="p-4 space-y-4">
|
||||
<SubAgentsTab
|
||||
values={values}
|
||||
onChange={setValues}
|
||||
getAgentNameById={getAgentNameById}
|
||||
editingAgentId={initialValues.id}
|
||||
clientId={clientId}
|
||||
/>
|
||||
</TabsContent>
|
||||
</ScrollArea>
|
||||
</Tabs>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
disabled={!values.name || isLoading}
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="animate-spin h-4 w-4 border-2 border-black border-t-transparent rounded-full mr-2"></div>
|
||||
)}
|
||||
{initialValues.id ? "Save Changes" : "Add Agent"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* MCP Dialog */}
|
||||
<MCPDialog
|
||||
open={mcpDialogOpen}
|
||||
onOpenChange={setMcpDialogOpen}
|
||||
onSave={handleSaveMCP}
|
||||
availableMCPs={availableMCPs}
|
||||
selectedMCP={
|
||||
availableMCPs.find((m) => selectedMCP?.id === m.id) || null
|
||||
}
|
||||
initialEnvs={selectedMCP?.envs || {}}
|
||||
initialTools={selectedMCP?.tools || []}
|
||||
clientId={clientId}
|
||||
/>
|
||||
|
||||
{/* Custom MCP Dialog */}
|
||||
<CustomMCPDialog
|
||||
open={customMcpDialogOpen}
|
||||
onOpenChange={setCustomMcpDialogOpen}
|
||||
onSave={handleSaveCustomMCP}
|
||||
initialCustomMCP={selectedCustomMCP}
|
||||
clientId={clientId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
243
frontend/app/agents/forms/BasicInfoTab.tsx
Normal file
243
frontend/app/agents/forms/BasicInfoTab.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/forms/BasicInfoTab.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 { AgentTypeSelector } from "@/app/agents/AgentTypeSelector";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Agent, AgentType } from "@/types/agent";
|
||||
import { ApiKey } from "@/services/agentService";
|
||||
import { A2AAgentConfig } from "../config/A2AAgentConfig";
|
||||
import { LLMAgentConfig } from "../config/LLMAgentConfig";
|
||||
import { sanitizeAgentName } from "@/lib/utils";
|
||||
|
||||
interface ModelOption {
|
||||
value: string;
|
||||
label: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
interface BasicInfoTabProps {
|
||||
values: Partial<Agent>;
|
||||
onChange: (values: Partial<Agent>) => void;
|
||||
apiKeys: ApiKey[];
|
||||
availableModels: ModelOption[];
|
||||
onOpenApiKeysDialog: () => void;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
export function BasicInfoTab({
|
||||
values,
|
||||
onChange,
|
||||
apiKeys,
|
||||
availableModels,
|
||||
onOpenApiKeysDialog,
|
||||
}: BasicInfoTabProps) {
|
||||
const handleNameBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
const sanitizedName = sanitizeAgentName(e.target.value);
|
||||
if (sanitizedName !== e.target.value) {
|
||||
onChange({ ...values, name: sanitizedName });
|
||||
}
|
||||
};
|
||||
|
||||
const handleTypeChange = (type: AgentType) => {
|
||||
let newValues: Partial<Agent> = { ...values, type };
|
||||
|
||||
if (type === "llm") {
|
||||
newValues = {
|
||||
...newValues,
|
||||
model: "openai/gpt-4.1-nano",
|
||||
instruction: "",
|
||||
role: "",
|
||||
goal: "",
|
||||
agent_card_url: undefined,
|
||||
config: {
|
||||
tools: [],
|
||||
mcp_servers: [],
|
||||
custom_mcp_servers: [],
|
||||
custom_tools: {
|
||||
http_tools: [],
|
||||
},
|
||||
sub_agents: [],
|
||||
},
|
||||
};
|
||||
} else if (type === "a2a") {
|
||||
newValues = {
|
||||
...newValues,
|
||||
model: undefined,
|
||||
instruction: undefined,
|
||||
role: undefined,
|
||||
goal: undefined,
|
||||
agent_card_url: "",
|
||||
api_key_id: undefined,
|
||||
config: undefined,
|
||||
};
|
||||
} else if (type === "loop") {
|
||||
newValues = {
|
||||
...newValues,
|
||||
model: undefined,
|
||||
instruction: undefined,
|
||||
role: undefined,
|
||||
goal: undefined,
|
||||
agent_card_url: undefined,
|
||||
api_key_id: undefined,
|
||||
config: {
|
||||
sub_agents: [],
|
||||
custom_mcp_servers: [],
|
||||
},
|
||||
};
|
||||
} else if (type === "workflow") {
|
||||
newValues = {
|
||||
...newValues,
|
||||
model: undefined,
|
||||
instruction: undefined,
|
||||
role: undefined,
|
||||
goal: undefined,
|
||||
agent_card_url: undefined,
|
||||
api_key_id: undefined,
|
||||
config: {
|
||||
sub_agents: [],
|
||||
workflow: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (type === "task") {
|
||||
newValues = {
|
||||
...newValues,
|
||||
model: undefined,
|
||||
instruction: undefined,
|
||||
role: undefined,
|
||||
goal: undefined,
|
||||
agent_card_url: undefined,
|
||||
api_key_id: undefined,
|
||||
config: {
|
||||
tasks: [],
|
||||
sub_agents: [],
|
||||
},
|
||||
};
|
||||
} else {
|
||||
newValues = {
|
||||
...newValues,
|
||||
model: undefined,
|
||||
instruction: undefined,
|
||||
role: undefined,
|
||||
goal: undefined,
|
||||
agent_card_url: undefined,
|
||||
api_key_id: undefined,
|
||||
config: {
|
||||
sub_agents: [],
|
||||
custom_mcp_servers: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
onChange(newValues);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="type" className="text-right text-neutral-300">
|
||||
Agent Type
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
<AgentTypeSelector
|
||||
value={values.type || "llm"}
|
||||
onValueChange={handleTypeChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right text-neutral-300">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={values.name || ""}
|
||||
onChange={(e) => onChange({ ...values, name: e.target.value })}
|
||||
onBlur={handleNameBlur}
|
||||
className="col-span-3 bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{values.type !== "a2a" && (
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="description" className="text-right text-neutral-300">
|
||||
Description
|
||||
</Label>
|
||||
<Input
|
||||
id="description"
|
||||
value={values.description || ""}
|
||||
onChange={(e) =>
|
||||
onChange({ ...values, description: e.target.value })
|
||||
}
|
||||
className="col-span-3 bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{values.type === "llm" && (
|
||||
<LLMAgentConfig
|
||||
apiKeys={apiKeys}
|
||||
availableModels={availableModels}
|
||||
values={values}
|
||||
onChange={onChange}
|
||||
onOpenApiKeysDialog={onOpenApiKeysDialog}
|
||||
/>
|
||||
)}
|
||||
|
||||
{values.type === "loop" && values.config?.max_iterations && (
|
||||
<div className="space-y-1 text-xs text-neutral-400">
|
||||
<div>
|
||||
<strong>Max. Iterations:</strong> {values.config.max_iterations}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{values.type === "workflow" && (
|
||||
<div className="space-y-1 text-xs text-neutral-400">
|
||||
<div>
|
||||
<strong>Type:</strong> Visual Flow
|
||||
</div>
|
||||
{values.config?.workflow && (
|
||||
<div>
|
||||
<strong>Elements:</strong>{" "}
|
||||
{values.config.workflow.nodes?.length || 0} nodes,{" "}
|
||||
{values.config.workflow.edges?.length || 0} connections
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
722
frontend/app/agents/forms/ConfigurationTab.tsx
Normal file
722
frontend/app/agents/forms/ConfigurationTab.tsx
Normal file
@@ -0,0 +1,722 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/forms/ConfigurationTab.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 { Agent } from "@/types/agent";
|
||||
import { MCPServer } from "@/types/mcpServer";
|
||||
import {
|
||||
Check,
|
||||
Copy,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Plus,
|
||||
Server,
|
||||
Settings,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { ParallelAgentConfig } from "../config/ParallelAgentConfig";
|
||||
import { SequentialAgentConfig } from "../config/SequentialAgentConfig";
|
||||
import { ApiKey } from "@/services/agentService";
|
||||
import { LoopAgentConfig } from "../config/LoopAgentConfig copy";
|
||||
import { A2AAgentConfig } from "../config/A2AAgentConfig";
|
||||
import { TaskAgentConfig } from "../config/TaskAgentConfig";
|
||||
import { useState } from "react";
|
||||
import { MCPDialog } from "../dialogs/MCPDialog";
|
||||
import { CustomMCPDialog } from "../dialogs/CustomMCPDialog";
|
||||
import { AgentToolDialog } from "../dialogs/AgentToolDialog";
|
||||
import { CustomToolDialog } from "../dialogs/CustomToolDialog";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
interface ConfigurationTabProps {
|
||||
values: Partial<Agent>;
|
||||
onChange: (values: Partial<Agent>) => void;
|
||||
agents: Agent[];
|
||||
availableMCPs: MCPServer[];
|
||||
apiKeys: ApiKey[];
|
||||
availableModels: any[];
|
||||
getAgentNameById: (id: string) => string;
|
||||
onOpenApiKeysDialog: () => void;
|
||||
onConfigureMCP: (mcpConfig: any) => void;
|
||||
onRemoveMCP: (mcpId: string) => void;
|
||||
onConfigureCustomMCP: (customMCP: any) => void;
|
||||
onRemoveCustomMCP: (url: string) => void;
|
||||
onOpenMCPDialog: (mcpConfig?: any) => void;
|
||||
onOpenCustomMCPDialog: (customMCP?: any) => void;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
export function ConfigurationTab({
|
||||
values,
|
||||
onChange,
|
||||
agents,
|
||||
availableMCPs,
|
||||
apiKeys,
|
||||
availableModels,
|
||||
getAgentNameById,
|
||||
onOpenApiKeysDialog,
|
||||
onConfigureMCP,
|
||||
onRemoveMCP,
|
||||
onConfigureCustomMCP,
|
||||
onRemoveCustomMCP,
|
||||
onOpenMCPDialog,
|
||||
onOpenCustomMCPDialog,
|
||||
clientId,
|
||||
}: ConfigurationTabProps) {
|
||||
const [agentToolDialogOpen, setAgentToolDialogOpen] = useState(false);
|
||||
const [customToolDialogOpen, setCustomToolDialogOpen] = useState(false);
|
||||
const [editingCustomTool, setEditingCustomTool] = useState<any>(null);
|
||||
const [showApiKey, setShowApiKey] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [copiedCardUrl, setCopiedCardUrl] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleAddAgentTool = (tool: { id: string }) => {
|
||||
const updatedAgentTools = [...(values.config?.agent_tools || [])];
|
||||
if (!updatedAgentTools.includes(tool.id)) {
|
||||
updatedAgentTools.push(tool.id);
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
agent_tools: updatedAgentTools,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleRemoveAgentTool = (id: string) => {
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
agent_tools: (values.config?.agent_tools || []).filter(
|
||||
(toolId) => toolId !== id
|
||||
),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Custom Tools handlers
|
||||
const handleAddCustomTool = (tool: any) => {
|
||||
const updatedTools = [...(values.config?.custom_tools?.http_tools || [])];
|
||||
updatedTools.push(tool);
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
custom_tools: {
|
||||
...(values.config?.custom_tools || { http_tools: [] }),
|
||||
http_tools: updatedTools,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
const handleEditCustomTool = (tool: any, idx: number) => {
|
||||
setEditingCustomTool({ ...tool, idx });
|
||||
setCustomToolDialogOpen(true);
|
||||
};
|
||||
const handleSaveEditCustomTool = (tool: any) => {
|
||||
const updatedTools = [...(values.config?.custom_tools?.http_tools || [])];
|
||||
if (editingCustomTool && typeof editingCustomTool.idx === "number") {
|
||||
updatedTools[editingCustomTool.idx] = tool;
|
||||
}
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
custom_tools: {
|
||||
...(values.config?.custom_tools || { http_tools: [] }),
|
||||
http_tools: updatedTools,
|
||||
},
|
||||
},
|
||||
});
|
||||
setEditingCustomTool(null);
|
||||
};
|
||||
const handleRemoveCustomTool = (idx: number) => {
|
||||
const updatedTools = [...(values.config?.custom_tools?.http_tools || [])];
|
||||
updatedTools.splice(idx, 1);
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
custom_tools: {
|
||||
...(values.config?.custom_tools || { http_tools: [] }),
|
||||
http_tools: updatedTools,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const apiKeyField = (
|
||||
<div className="space-y-2 mb-4">
|
||||
<h3 className="text-lg font-medium text-white">Credentials</h3>
|
||||
<div className="border border-[#444] rounded-md p-4 bg-[#222] flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label
|
||||
className="text-sm text-neutral-400 mb-1"
|
||||
htmlFor="agent-card-url"
|
||||
>
|
||||
Agent URL. This URL can be used to access the agent card externally.
|
||||
</label>
|
||||
<div className="relative flex items-center">
|
||||
<input
|
||||
id="agent-card-url"
|
||||
type="text"
|
||||
className="w-full bg-[#2a2a2a] border border-[#444] rounded-md px-3 py-2 text-white pr-12 focus:outline-none focus:ring-2 focus:ring-emerald-400/40"
|
||||
value={
|
||||
values?.agent_card_url?.replace(
|
||||
"/.well-known/agent.json",
|
||||
""
|
||||
) || ""
|
||||
}
|
||||
disabled
|
||||
autoComplete="off"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 text-neutral-400 hover:text-emerald-400 px-1 py-1"
|
||||
onClick={async () => {
|
||||
if (values?.agent_card_url) {
|
||||
await navigator.clipboard.writeText(
|
||||
values.agent_card_url.replace("/.well-known/agent.json", "")
|
||||
);
|
||||
setCopiedCardUrl(true);
|
||||
setTimeout(() => setCopiedCardUrl(false), 1200);
|
||||
toast({
|
||||
title: "Copied!",
|
||||
description:
|
||||
"The agent URL was copied to the clipboard.",
|
||||
});
|
||||
}
|
||||
}}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{copiedCardUrl ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm text-neutral-400 mb-1" htmlFor="agent-api_key">
|
||||
Configure the API Key for this agent. This key will be used for
|
||||
authentication with external services.
|
||||
</label>
|
||||
<div className="relative flex items-center">
|
||||
<input
|
||||
id="agent-api_key"
|
||||
type={showApiKey ? "text" : "password"}
|
||||
className="w-full bg-[#2a2a2a] border border-[#444] rounded-md px-3 py-2 text-white pr-24 focus:outline-none focus:ring-2 focus:ring-emerald-400/40"
|
||||
value={values.config?.api_key || ""}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
api_key: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-9 text-neutral-400 hover:text-emerald-400 px-1 py-1"
|
||||
onClick={() => setShowApiKey((v) => !v)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showApiKey ? (
|
||||
<EyeOff className="w-4 h-4" />
|
||||
) : (
|
||||
<Eye className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 text-neutral-400 hover:text-emerald-400 px-1 py-1"
|
||||
onClick={async () => {
|
||||
if (values.config?.api_key) {
|
||||
await navigator.clipboard.writeText(values.config.api_key);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1200);
|
||||
toast({
|
||||
title: "Copied!",
|
||||
description:
|
||||
"The API key was copied to the clipboard.",
|
||||
});
|
||||
}
|
||||
}}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (values.type === "llm") {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{apiKeyField}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium text-white">MCP Servers</h3>
|
||||
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||
<p className="text-sm text-neutral-400 mb-4">
|
||||
Configure the MCP servers that this agent can use.
|
||||
</p>
|
||||
|
||||
{values.config?.mcp_servers &&
|
||||
values.config.mcp_servers.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{values.config.mcp_servers.map((mcpConfig) => {
|
||||
const mcpServer = availableMCPs.find(
|
||||
(mcp) => mcp.id === mcpConfig.id
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={mcpConfig.id}
|
||||
className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium text-white">
|
||||
{mcpServer?.name || mcpConfig.id}
|
||||
</p>
|
||||
<p className="text-sm text-neutral-400">
|
||||
{mcpServer?.description?.substring(0, 100)}
|
||||
...
|
||||
</p>
|
||||
{mcpConfig.tools && mcpConfig.tools.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{mcpConfig.tools.map((toolId) => (
|
||||
<Badge
|
||||
key={toolId}
|
||||
variant="outline"
|
||||
className="text-xs bg-[#333] text-emerald-400 border-emerald-400/30"
|
||||
>
|
||||
{toolId}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onConfigureMCP(mcpConfig)}
|
||||
className="flex items-center text-neutral-300 hover:text-emerald-400 hover:bg-[#333]"
|
||||
>
|
||||
<Settings className="h-4 w-4 mr-1" /> Configure
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onRemoveMCP(mcpConfig.id)}
|
||||
className="text-red-500 hover:text-red-400 hover:bg-[#333]"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onOpenMCPDialog(null)}
|
||||
className="w-full mt-2 border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add MCP Server
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md mb-2">
|
||||
<div>
|
||||
<p className="font-medium text-white">
|
||||
No MCP servers configured
|
||||
</p>
|
||||
<p className="text-sm text-neutral-400">
|
||||
Add MCP servers for this agent
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onOpenMCPDialog(null)}
|
||||
className="border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium text-white">Custom MCPs</h3>
|
||||
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||
<p className="text-sm text-neutral-400 mb-4">
|
||||
Configure custom MCPs with URL and HTTP headers.
|
||||
</p>
|
||||
|
||||
{values.config?.custom_mcp_servers &&
|
||||
values.config.custom_mcp_servers.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{values.config.custom_mcp_servers.map((customMCP) => (
|
||||
<div
|
||||
key={customMCP.url}
|
||||
className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium text-white">{customMCP.url}</p>
|
||||
<p className="text-sm text-neutral-400">
|
||||
{Object.keys(customMCP.headers || {}).length > 0
|
||||
? `${
|
||||
Object.keys(customMCP.headers || {}).length
|
||||
} headers configured`
|
||||
: "No headers configured"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onConfigureCustomMCP(customMCP)}
|
||||
className="flex items-center text-neutral-300 hover:text-emerald-400 hover:bg-[#333]"
|
||||
>
|
||||
<Settings className="h-4 w-4 mr-1" /> Configure
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onRemoveCustomMCP(customMCP.url)}
|
||||
className="text-red-500 hover:text-red-400 hover:bg-[#333]"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onOpenCustomMCPDialog(null)}
|
||||
className="w-full mt-2 border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add Custom MCP
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md mb-2">
|
||||
<div>
|
||||
<p className="font-medium text-white">
|
||||
No custom MCPs configured
|
||||
</p>
|
||||
<p className="text-sm text-neutral-400">
|
||||
Add custom MCPs for this agent
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onOpenCustomMCPDialog(null)}
|
||||
className="border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium text-white">Agent Tools</h3>
|
||||
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||
<p className="text-sm text-neutral-400 mb-4">
|
||||
Configure other agents as tools for this agent.
|
||||
</p>
|
||||
{values.config?.agent_tools &&
|
||||
values.config.agent_tools.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{values.config.agent_tools.map((toolId) => {
|
||||
const agent = agents.find((a) => a.id === toolId);
|
||||
return (
|
||||
<div
|
||||
key={toolId}
|
||||
className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium text-white">
|
||||
{agent?.name || toolId}
|
||||
</p>
|
||||
<p className="text-sm text-neutral-400">
|
||||
{agent?.description || "No description"}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveAgentTool(toolId)}
|
||||
className="text-red-500 hover:text-red-400 hover:bg-[#333]"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setAgentToolDialogOpen(true)}
|
||||
className="w-full mt-2 border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add Agent Tool
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md mb-2">
|
||||
<div>
|
||||
<p className="font-medium text-white">
|
||||
No agent tools configured
|
||||
</p>
|
||||
<p className="text-sm text-neutral-400">
|
||||
Add agent tools for this agent
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setAgentToolDialogOpen(true)}
|
||||
className="border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium text-white">
|
||||
Custom Tools (HTTP Tools)
|
||||
</h3>
|
||||
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||
<p className="text-sm text-neutral-400 mb-4">
|
||||
Configure HTTP tools for this agent.
|
||||
</p>
|
||||
{values.config?.custom_tools?.http_tools &&
|
||||
values.config.custom_tools.http_tools.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{values.config.custom_tools.http_tools.map((tool, idx) => (
|
||||
<div
|
||||
key={tool.name + idx}
|
||||
className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium text-white">{tool.name}</p>
|
||||
<p className="text-xs text-neutral-400">
|
||||
{tool.method} {tool.endpoint}
|
||||
</p>
|
||||
<p className="text-xs text-neutral-400">
|
||||
{tool.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditCustomTool(tool, idx)}
|
||||
className="flex items-center text-neutral-300 hover:text-emerald-400 hover:bg-[#333]"
|
||||
>
|
||||
<span className="mr-1">Edit</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveCustomTool(idx)}
|
||||
className="text-red-500 hover:text-red-400 hover:bg-[#333]"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setEditingCustomTool(null);
|
||||
setCustomToolDialogOpen(true);
|
||||
}}
|
||||
className="w-full mt-2 border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add Custom Tool
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md mb-2">
|
||||
<div>
|
||||
<p className="font-medium text-white">
|
||||
No custom tools configured
|
||||
</p>
|
||||
<p className="text-sm text-neutral-400">
|
||||
Add HTTP tools for this agent
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setEditingCustomTool(null);
|
||||
setCustomToolDialogOpen(true);
|
||||
}}
|
||||
className="border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<CustomToolDialog
|
||||
open={customToolDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setCustomToolDialogOpen(open);
|
||||
if (!open) setEditingCustomTool(null);
|
||||
}}
|
||||
onSave={
|
||||
editingCustomTool ? handleSaveEditCustomTool : handleAddCustomTool
|
||||
}
|
||||
initialTool={editingCustomTool}
|
||||
/>
|
||||
{agentToolDialogOpen && (
|
||||
<AgentToolDialog
|
||||
open={agentToolDialogOpen}
|
||||
onOpenChange={setAgentToolDialogOpen}
|
||||
onSave={handleAddAgentTool}
|
||||
currentAgentId={values.id}
|
||||
folderId={values.folder_id}
|
||||
clientId={clientId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (values.type === "a2a") {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{apiKeyField}
|
||||
<A2AAgentConfig values={values} onChange={onChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (values.type === "sequential") {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{apiKeyField}
|
||||
<SequentialAgentConfig
|
||||
values={values}
|
||||
onChange={onChange}
|
||||
agents={agents}
|
||||
getAgentNameById={getAgentNameById}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (values.type === "parallel") {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{apiKeyField}
|
||||
<ParallelAgentConfig
|
||||
values={values}
|
||||
onChange={onChange}
|
||||
agents={agents}
|
||||
getAgentNameById={getAgentNameById}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (values.type === "loop") {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{apiKeyField}
|
||||
<LoopAgentConfig
|
||||
values={values}
|
||||
onChange={onChange}
|
||||
agents={agents}
|
||||
getAgentNameById={getAgentNameById}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (values.type === "task") {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{apiKeyField}
|
||||
<TaskAgentConfig
|
||||
values={values}
|
||||
onChange={onChange}
|
||||
agents={agents}
|
||||
getAgentNameById={getAgentNameById}
|
||||
singleTask={values.type === "task"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{apiKeyField}
|
||||
<div className="flex items-center justify-center h-40">
|
||||
<div className="text-center">
|
||||
<p className="text-neutral-400">
|
||||
Configure the sub-agents in the "Sub-Agents" tab
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
246
frontend/app/agents/forms/SubAgentsTab.tsx
Normal file
246
frontend/app/agents/forms/SubAgentsTab.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/forms/SubAgentsTab.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 { useState, useEffect } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { listAgents } from "@/services/agentService";
|
||||
import { Loader2, Search, X } from "lucide-react";
|
||||
|
||||
interface SubAgentsTabProps {
|
||||
values: Partial<Agent>;
|
||||
onChange: (values: Partial<Agent>) => void;
|
||||
getAgentNameById: (id: string) => string;
|
||||
editingAgentId?: string;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
export function SubAgentsTab({
|
||||
values,
|
||||
onChange,
|
||||
getAgentNameById,
|
||||
editingAgentId,
|
||||
clientId,
|
||||
}: SubAgentsTabProps) {
|
||||
const [availableAgents, setAvailableAgents] = useState<Agent[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
// Get folder ID from current agent
|
||||
const folderId = values.folder_id;
|
||||
|
||||
useEffect(() => {
|
||||
loadAgents();
|
||||
}, [clientId, folderId, editingAgentId]);
|
||||
|
||||
const loadAgents = async () => {
|
||||
if (!clientId) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await listAgents(
|
||||
clientId,
|
||||
0,
|
||||
100,
|
||||
folderId || undefined
|
||||
);
|
||||
|
||||
// Filter out the current agent to avoid self-reference
|
||||
const filteredAgents = res.data.filter(agent =>
|
||||
agent.id !== editingAgentId
|
||||
);
|
||||
|
||||
setAvailableAgents(filteredAgents);
|
||||
} catch (error) {
|
||||
console.error("Error loading agents:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddSubAgent = (agentId: string) => {
|
||||
if (!values.config?.sub_agents?.includes(agentId)) {
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...values.config,
|
||||
sub_agents: [...(values.config?.sub_agents || []), agentId],
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveSubAgent = (agentId: string) => {
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...values.config,
|
||||
sub_agents:
|
||||
values.config?.sub_agents?.filter((id) => id !== agentId) || [],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const filteredAgents = availableAgents.filter(agent =>
|
||||
agent.name.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-medium text-white">Sub-Agents</h3>
|
||||
<div className="text-sm text-neutral-400">
|
||||
{values.config?.sub_agents?.length || 0} sub-agents selected
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||
<p className="text-sm text-neutral-400 mb-4">
|
||||
Select the agents that will be used as sub-agents.
|
||||
</p>
|
||||
|
||||
{values.config?.sub_agents && values.config.sub_agents.length > 0 ? (
|
||||
<div className="space-y-2 mb-4">
|
||||
<h4 className="text-sm font-medium text-white">
|
||||
Selected sub-agents:
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{values.config.sub_agents.map((agentId) => (
|
||||
<Badge
|
||||
key={agentId}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-1 bg-[#333] text-emerald-400"
|
||||
>
|
||||
{getAgentNameById(agentId)}
|
||||
<button
|
||||
onClick={() => handleRemoveSubAgent(agentId)}
|
||||
className="ml-1 h-4 w-4 rounded-full hover:bg-[#444] inline-flex items-center justify-center"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-4 text-neutral-400 mb-4">
|
||||
No sub-agents selected
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-4">
|
||||
<h4 className="text-sm font-medium text-white mb-2">
|
||||
Available agents:
|
||||
</h4>
|
||||
<div className="relative mb-3">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-neutral-500" />
|
||||
<Input
|
||||
placeholder="Search agents by name..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="pl-9 bg-[#1a1a1a] border-[#444] text-white"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
onClick={() => setSearch("")}
|
||||
className="absolute right-2.5 top-2.5 text-neutral-400 hover:text-white"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-6">
|
||||
<Loader2 className="h-6 w-6 text-emerald-400 animate-spin" />
|
||||
<div className="mt-2 text-sm text-neutral-400">Loading agents...</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 max-h-60 overflow-y-auto">
|
||||
{filteredAgents.length === 0 ? (
|
||||
<div className="text-center py-4 text-neutral-400">
|
||||
{search ? `No agents found matching "${search}"` : "No other agents found in this folder"}
|
||||
</div>
|
||||
) : (
|
||||
filteredAgents.map((agent) => (
|
||||
<div
|
||||
key={agent.id}
|
||||
className="flex items-center justify-between p-2 hover:bg-[#2a2a2a] rounded-md"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-white">{agent.name}</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="ml-2 border-[#444] text-emerald-400"
|
||||
>
|
||||
{agent.type === "llm"
|
||||
? "LLM Agent"
|
||||
: agent.type === "a2a"
|
||||
? "A2A Agent"
|
||||
: agent.type === "sequential"
|
||||
? "Sequential Agent"
|
||||
: agent.type === "parallel"
|
||||
? "Parallel Agent"
|
||||
: agent.type === "loop"
|
||||
? "Loop Agent"
|
||||
: agent.type === "workflow"
|
||||
? "Workflow Agent"
|
||||
: agent.type === "task"
|
||||
? "Task Agent"
|
||||
: agent.type}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleAddSubAgent(agent.id)}
|
||||
disabled={values.config?.sub_agents?.includes(agent.id)}
|
||||
className={
|
||||
values.config?.sub_agents?.includes(agent.id)
|
||||
? "text-neutral-500 bg-[#222] hover:bg-[#333]"
|
||||
: "text-emerald-400 hover:bg-[#333] bg-[#222]"
|
||||
}
|
||||
>
|
||||
{values.config?.sub_agents?.includes(agent.id)
|
||||
? "Added"
|
||||
: "Add"}
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
754
frontend/app/agents/page.tsx
Normal file
754
frontend/app/agents/page.tsx
Normal file
@@ -0,0 +1,754 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/page.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 { useState, useEffect, useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Key, Plus, Folder, Download, Upload } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { exportAsJson } from "@/lib/utils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
import { Agent, AgentCreate } from "@/types/agent";
|
||||
import { Folder as AgentFolder } from "@/services/agentService";
|
||||
import {
|
||||
listAgents,
|
||||
createAgent,
|
||||
updateAgent,
|
||||
deleteAgent,
|
||||
listFolders,
|
||||
createFolder,
|
||||
updateFolder,
|
||||
deleteFolder,
|
||||
assignAgentToFolder,
|
||||
ApiKey,
|
||||
listApiKeys,
|
||||
createApiKey,
|
||||
updateApiKey,
|
||||
deleteApiKey,
|
||||
shareAgent,
|
||||
importAgentFromJson,
|
||||
} from "@/services/agentService";
|
||||
import { listMCPServers } from "@/services/mcpServerService";
|
||||
import { AgentSidebar } from "./AgentSidebar";
|
||||
import { SearchInput } from "./SearchInput";
|
||||
import { AgentList } from "./AgentList";
|
||||
import { EmptyState } from "./EmptyState";
|
||||
import { AgentForm } from "./forms/AgentForm";
|
||||
import { FolderDialog } from "./dialogs/FolderDialog";
|
||||
import { MoveAgentDialog } from "./dialogs/MoveAgentDialog";
|
||||
import { ConfirmationDialog } from "./dialogs/ConfirmationDialog";
|
||||
import { ApiKeysDialog } from "./dialogs/ApiKeysDialog";
|
||||
import { ShareAgentDialog } from "./dialogs/ShareAgentDialog";
|
||||
import { MCPServer } from "@/types/mcpServer";
|
||||
import { availableModels } from "@/types/aiModels";
|
||||
import { ImportAgentDialog } from "./dialogs/ImportAgentDialog";
|
||||
|
||||
export default function AgentsPage() {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const user =
|
||||
typeof window !== "undefined"
|
||||
? JSON.parse(localStorage.getItem("user") || "{}")
|
||||
: {};
|
||||
const clientId = user?.client_id || "";
|
||||
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [filteredAgents, setFilteredAgents] = useState<Agent[]>([]);
|
||||
const [folders, setFolders] = useState<AgentFolder[]>([]);
|
||||
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
|
||||
const [availableMCPs, setAvailableMCPs] = useState<MCPServer[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [selectedAgentType, setSelectedAgentType] = useState<string | null>(null);
|
||||
const [agentTypes, setAgentTypes] = useState<string[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [isSidebarVisible, setIsSidebarVisible] = useState(false);
|
||||
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null);
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [isFolderDialogOpen, setIsFolderDialogOpen] = useState(false);
|
||||
const [isMovingDialogOpen, setIsMovingDialogOpen] = useState(false);
|
||||
const [isDeleteAgentDialogOpen, setIsDeleteAgentDialogOpen] = useState(false);
|
||||
const [isDeleteFolderDialogOpen, setIsDeleteFolderDialogOpen] =
|
||||
useState(false);
|
||||
const [isApiKeysDialogOpen, setIsApiKeysDialogOpen] = useState(false);
|
||||
const [isMCPDialogOpen, setIsMCPDialogOpen] = useState(false);
|
||||
const [isCustomMCPDialogOpen, setIsCustomMCPDialogOpen] = useState(false);
|
||||
const [isShareDialogOpen, setIsShareDialogOpen] = useState(false);
|
||||
|
||||
const [editingAgent, setEditingAgent] = useState<Agent | null>(null);
|
||||
const [editingFolder, setEditingFolder] = useState<AgentFolder | null>(null);
|
||||
const [agentToDelete, setAgentToDelete] = useState<Agent | null>(null);
|
||||
const [agentToMove, setAgentToMove] = useState<Agent | null>(null);
|
||||
const [agentToShare, setAgentToShare] = useState<Agent | null>(null);
|
||||
const [sharedApiKey, setSharedApiKey] = useState<string>("");
|
||||
const [folderToDelete, setFolderToDelete] = useState<AgentFolder | null>(null);
|
||||
|
||||
const [newAgent, setNewAgent] = useState<Partial<Agent>>({
|
||||
client_id: clientId || "",
|
||||
name: "",
|
||||
description: "",
|
||||
type: "llm",
|
||||
model: "openai/gpt-4.1-nano",
|
||||
instruction: "",
|
||||
api_key_id: "",
|
||||
config: {
|
||||
tools: [],
|
||||
mcp_servers: [],
|
||||
custom_mcp_servers: [],
|
||||
custom_tools: {
|
||||
http_tools: [],
|
||||
},
|
||||
sub_agents: [],
|
||||
agent_tools: [],
|
||||
},
|
||||
});
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [isImportDialogOpen, setIsImportDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clientId) return;
|
||||
loadAgents();
|
||||
loadFolders();
|
||||
loadApiKeys();
|
||||
}, [clientId, selectedFolderId]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadMCPs = async () => {
|
||||
try {
|
||||
const res = await listMCPServers();
|
||||
setAvailableMCPs(res.data);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error loading MCP servers",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadMCPs();
|
||||
}, []);
|
||||
|
||||
const loadAgents = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await listAgents(
|
||||
clientId,
|
||||
0,
|
||||
100,
|
||||
selectedFolderId || undefined
|
||||
);
|
||||
setAgents(res.data);
|
||||
setFilteredAgents(res.data);
|
||||
|
||||
// Extract unique agent types
|
||||
const types = [...new Set(res.data.map(agent => agent.type))].filter(Boolean);
|
||||
setAgentTypes(types);
|
||||
} catch (error) {
|
||||
toast({ title: "Error loading agents", variant: "destructive" });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadFolders = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await listFolders(clientId);
|
||||
setFolders(res.data);
|
||||
} catch (error) {
|
||||
toast({ title: "Error loading folders", variant: "destructive" });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadApiKeys = async () => {
|
||||
try {
|
||||
const res = await listApiKeys(clientId);
|
||||
setApiKeys(res.data);
|
||||
} catch (error) {
|
||||
toast({ title: "Error loading API keys", variant: "destructive" });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Apply both search term and type filters
|
||||
let filtered = [...agents];
|
||||
|
||||
// Apply search term filter
|
||||
if (searchTerm.trim() !== "") {
|
||||
const lowercaseSearch = searchTerm.toLowerCase();
|
||||
filtered = filtered.filter(
|
||||
(agent) =>
|
||||
agent.name.toLowerCase().includes(lowercaseSearch) ||
|
||||
agent.description?.toLowerCase().includes(lowercaseSearch)
|
||||
);
|
||||
}
|
||||
|
||||
// Apply agent type filter
|
||||
if (selectedAgentType) {
|
||||
filtered = filtered.filter(agent => agent.type === selectedAgentType);
|
||||
}
|
||||
|
||||
setFilteredAgents(filtered);
|
||||
}, [searchTerm, selectedAgentType, agents]);
|
||||
|
||||
const handleAddAgent = async (agentData: Partial<Agent>) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
if (editingAgent) {
|
||||
await updateAgent(editingAgent.id, {
|
||||
...agentData,
|
||||
client_id: clientId,
|
||||
});
|
||||
toast({
|
||||
title: "Agent updated",
|
||||
description: `${agentData.name} was updated successfully`,
|
||||
});
|
||||
} else {
|
||||
const createdAgent = await createAgent({
|
||||
...(agentData as AgentCreate),
|
||||
client_id: clientId,
|
||||
});
|
||||
|
||||
if (selectedFolderId && createdAgent.data.id) {
|
||||
await assignAgentToFolder(
|
||||
createdAgent.data.id,
|
||||
selectedFolderId,
|
||||
clientId
|
||||
);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Agent added",
|
||||
description: `${agentData.name} was added successfully`,
|
||||
});
|
||||
}
|
||||
loadAgents();
|
||||
setIsDialogOpen(false);
|
||||
resetForm();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Unable to save agent",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAgent = async () => {
|
||||
if (!agentToDelete) return;
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await deleteAgent(agentToDelete.id);
|
||||
toast({
|
||||
title: "Agent deleted",
|
||||
description: "The agent was deleted successfully",
|
||||
});
|
||||
loadAgents();
|
||||
setAgentToDelete(null);
|
||||
setIsDeleteAgentDialogOpen(false);
|
||||
} catch {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Unable to delete agent",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditAgent = (agent: Agent) => {
|
||||
setEditingAgent(agent);
|
||||
setNewAgent({ ...agent });
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleMoveAgent = async (targetFolderId: string | null) => {
|
||||
if (!agentToMove) return;
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await assignAgentToFolder(agentToMove.id, targetFolderId, clientId);
|
||||
toast({
|
||||
title: "Agent moved",
|
||||
description: targetFolderId
|
||||
? `Agent moved to folder successfully`
|
||||
: "Agent removed from folder successfully",
|
||||
});
|
||||
setIsMovingDialogOpen(false);
|
||||
loadAgents();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Unable to move agent",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setAgentToMove(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddFolder = async (folderData: {
|
||||
name: string;
|
||||
description: string;
|
||||
}) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
if (editingFolder) {
|
||||
await updateFolder(editingFolder.id, folderData, clientId);
|
||||
toast({
|
||||
title: "Folder updated",
|
||||
description: `${folderData.name} was updated successfully`,
|
||||
});
|
||||
} else {
|
||||
await createFolder({
|
||||
...folderData,
|
||||
client_id: clientId,
|
||||
});
|
||||
toast({
|
||||
title: "Folder created",
|
||||
description: `${folderData.name} was created successfully`,
|
||||
});
|
||||
}
|
||||
loadFolders();
|
||||
setIsFolderDialogOpen(false);
|
||||
setEditingFolder(null);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Unable to save folder",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteFolder = async () => {
|
||||
if (!folderToDelete) return;
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await deleteFolder(folderToDelete.id, clientId);
|
||||
toast({
|
||||
title: "Folder deleted",
|
||||
description: "The folder was deleted successfully",
|
||||
});
|
||||
loadFolders();
|
||||
if (selectedFolderId === folderToDelete.id) {
|
||||
setSelectedFolderId(null);
|
||||
}
|
||||
setFolderToDelete(null);
|
||||
setIsDeleteFolderDialogOpen(false);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Unable to delete folder",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShareAgent = async (agent: Agent) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setAgentToShare(agent);
|
||||
const response = await shareAgent(agent.id, clientId);
|
||||
|
||||
if (response.data && response.data.api_key) {
|
||||
setSharedApiKey(response.data.api_key);
|
||||
setIsShareDialogOpen(true);
|
||||
|
||||
toast({
|
||||
title: "Agent shared",
|
||||
description: "API key generated successfully",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Unable to share agent",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setNewAgent({
|
||||
client_id: clientId || "",
|
||||
name: "",
|
||||
description: "",
|
||||
type: "llm",
|
||||
model: "openai/gpt-4.1-nano",
|
||||
instruction: "",
|
||||
api_key_id: "",
|
||||
config: {
|
||||
tools: [],
|
||||
mcp_servers: [],
|
||||
custom_mcp_servers: [],
|
||||
custom_tools: {
|
||||
http_tools: [],
|
||||
},
|
||||
sub_agents: [],
|
||||
agent_tools: [],
|
||||
},
|
||||
});
|
||||
setEditingAgent(null);
|
||||
};
|
||||
|
||||
// Function to export all agents as JSON
|
||||
const handleExportAllAgents = () => {
|
||||
try {
|
||||
// Create file name with current date
|
||||
const date = new Date();
|
||||
const formattedDate = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
|
||||
const filename = `agents-export-${formattedDate}`;
|
||||
|
||||
// Use the utility function to export
|
||||
// Pass agents both as the data and as allAgents parameter to properly resolve references
|
||||
const result = exportAsJson({ agents: filteredAgents }, filename, true, agents);
|
||||
|
||||
if (result) {
|
||||
toast({
|
||||
title: "Export complete",
|
||||
description: `${filteredAgents.length} agent(s) exported to JSON`,
|
||||
});
|
||||
} else {
|
||||
throw new Error("Export failed");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error exporting agents:", error);
|
||||
|
||||
toast({
|
||||
title: "Export failed",
|
||||
description: "There was an error exporting the agents",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportAgentJSON = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file || !clientId) return;
|
||||
|
||||
try {
|
||||
setIsImporting(true);
|
||||
|
||||
await importAgentFromJson(file, clientId);
|
||||
|
||||
toast({
|
||||
title: "Import successful",
|
||||
description: "Agent was imported successfully",
|
||||
});
|
||||
|
||||
// Refresh the agent list
|
||||
loadAgents();
|
||||
|
||||
// Reset file input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error importing agent:", error);
|
||||
toast({
|
||||
title: "Import failed",
|
||||
description: "There was an error importing the agent",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6 bg-[#121212] min-h-screen flex relative">
|
||||
<AgentSidebar
|
||||
visible={isSidebarVisible}
|
||||
folders={folders}
|
||||
selectedFolderId={selectedFolderId}
|
||||
onSelectFolder={setSelectedFolderId}
|
||||
onAddFolder={() => {
|
||||
setEditingFolder(null);
|
||||
setIsFolderDialogOpen(true);
|
||||
}}
|
||||
onEditFolder={(folder) => {
|
||||
setEditingFolder(folder as AgentFolder);
|
||||
setIsFolderDialogOpen(true);
|
||||
}}
|
||||
onDeleteFolder={(folder) => {
|
||||
setFolderToDelete(folder as AgentFolder);
|
||||
setIsDeleteFolderDialogOpen(true);
|
||||
}}
|
||||
onClose={() => setIsSidebarVisible(!isSidebarVisible)}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`w-full transition-all duration-300 ease-in-out ${
|
||||
isSidebarVisible ? "pl-64" : "pl-0"
|
||||
}`}
|
||||
>
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
{!isSidebarVisible && (
|
||||
<button
|
||||
onClick={() => setIsSidebarVisible(true)}
|
||||
className="mr-2 bg-[#222] p-2 rounded-md text-emerald-400 hover:bg-[#333] hover:text-emerald-400 shadow-md transition-all"
|
||||
aria-label="Show folders"
|
||||
>
|
||||
<Folder className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
<h1 className="text-3xl font-bold text-white flex items-center ml-2">
|
||||
{selectedFolderId
|
||||
? folders.find((f) => f.id === selectedFolderId)?.name
|
||||
: "Agents"}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<SearchInput
|
||||
value={searchTerm}
|
||||
onChange={setSearchTerm}
|
||||
placeholder="Search agents..."
|
||||
selectedAgentType={selectedAgentType}
|
||||
onAgentTypeChange={setSelectedAgentType}
|
||||
agentTypes={agentTypes}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={() => setIsApiKeysDialogOpen(true)}
|
||||
className="bg-[#222] text-white hover:bg-[#333] border border-[#444]"
|
||||
>
|
||||
<Key className="mr-2 h-4 w-4 text-emerald-400" />
|
||||
API Keys
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleExportAllAgents}
|
||||
className="bg-[#222] text-white hover:bg-[#333] border border-[#444]"
|
||||
title="Export all agents as JSON"
|
||||
>
|
||||
<Download className="mr-2 h-4 w-4 text-purple-400" />
|
||||
Export All
|
||||
</Button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="bg-emerald-400 text-black hover:bg-[#00cc7d]">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Agent
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="bg-zinc-900 border-zinc-700">
|
||||
<DropdownMenuItem
|
||||
className="text-white hover:bg-zinc-800 cursor-pointer"
|
||||
onClick={() => {
|
||||
resetForm();
|
||||
setIsDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2 text-emerald-400" />
|
||||
New Agent
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-white hover:bg-zinc-800 cursor-pointer"
|
||||
onClick={() => setIsImportDialogOpen(true)}
|
||||
>
|
||||
<Upload className="h-4 w-4 mr-2 text-indigo-400" />
|
||||
Import Agent JSON
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-[60vh]">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-emerald-400"></div>
|
||||
</div>
|
||||
) : filteredAgents.length > 0 ? (
|
||||
<AgentList
|
||||
agents={filteredAgents}
|
||||
isLoading={isLoading}
|
||||
searchTerm={searchTerm}
|
||||
selectedFolderId={selectedFolderId}
|
||||
availableMCPs={availableMCPs}
|
||||
getApiKeyNameById={(id) =>
|
||||
apiKeys.find((k) => k.id === id)?.name || null
|
||||
}
|
||||
getAgentNameById={(id) =>
|
||||
agents.find((a) => a.id === id)?.name || id
|
||||
}
|
||||
onEdit={handleEditAgent}
|
||||
onDelete={(agent) => {
|
||||
setAgentToDelete(agent);
|
||||
setIsDeleteAgentDialogOpen(true);
|
||||
}}
|
||||
onMove={(agent) => {
|
||||
setAgentToMove(agent);
|
||||
setIsMovingDialogOpen(true);
|
||||
}}
|
||||
onShare={handleShareAgent}
|
||||
onClearSearch={() => {
|
||||
setSearchTerm("");
|
||||
setSelectedAgentType(null);
|
||||
}}
|
||||
onCreateAgent={() => {
|
||||
resetForm();
|
||||
setIsDialogOpen(true);
|
||||
}}
|
||||
onWorkflow={(agentId) => {
|
||||
router.push(`/agents/workflows?agentId=${agentId}`);
|
||||
}}
|
||||
apiKeys={apiKeys}
|
||||
folders={folders}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
type={
|
||||
searchTerm || selectedAgentType
|
||||
? "search-no-results"
|
||||
: selectedFolderId
|
||||
? "empty-folder"
|
||||
: "no-agents"
|
||||
}
|
||||
searchTerm={searchTerm}
|
||||
onAction={() => {
|
||||
searchTerm || selectedAgentType
|
||||
? (setSearchTerm(""), setSelectedAgentType(null))
|
||||
: (resetForm(), setIsDialogOpen(true));
|
||||
}}
|
||||
actionLabel={searchTerm || selectedAgentType ? "Clear filters" : "Create Agent"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AgentForm
|
||||
open={isDialogOpen}
|
||||
onOpenChange={setIsDialogOpen}
|
||||
initialValues={newAgent}
|
||||
apiKeys={apiKeys}
|
||||
availableModels={availableModels}
|
||||
availableMCPs={availableMCPs}
|
||||
agents={agents}
|
||||
onOpenApiKeysDialog={() => setIsApiKeysDialogOpen(true)}
|
||||
onOpenMCPDialog={() => setIsMCPDialogOpen(true)}
|
||||
onOpenCustomMCPDialog={() => setIsCustomMCPDialogOpen(true)}
|
||||
onSave={handleAddAgent}
|
||||
getAgentNameById={(id) => agents.find((a) => a.id === id)?.name || id}
|
||||
clientId={clientId}
|
||||
/>
|
||||
|
||||
<FolderDialog
|
||||
open={isFolderDialogOpen}
|
||||
onOpenChange={setIsFolderDialogOpen}
|
||||
editingFolder={editingFolder}
|
||||
onSave={handleAddFolder}
|
||||
/>
|
||||
|
||||
<MoveAgentDialog
|
||||
open={isMovingDialogOpen}
|
||||
onOpenChange={setIsMovingDialogOpen}
|
||||
agent={agentToMove}
|
||||
folders={folders}
|
||||
onMove={handleMoveAgent}
|
||||
/>
|
||||
|
||||
<ConfirmationDialog
|
||||
open={isDeleteAgentDialogOpen}
|
||||
onOpenChange={setIsDeleteAgentDialogOpen}
|
||||
title="Confirm deletion"
|
||||
description={`Are you sure you want to delete the agent "${agentToDelete?.name}"? This action cannot be undone.`}
|
||||
onConfirm={handleDeleteAgent}
|
||||
/>
|
||||
|
||||
<ConfirmationDialog
|
||||
open={isDeleteFolderDialogOpen}
|
||||
onOpenChange={setIsDeleteFolderDialogOpen}
|
||||
title="Confirm deletion"
|
||||
description={`Are you sure you want to delete the folder "${folderToDelete?.name}"? This action cannot be undone.`}
|
||||
confirmText="Delete"
|
||||
confirmVariant="destructive"
|
||||
onConfirm={handleDeleteFolder}
|
||||
/>
|
||||
|
||||
<ApiKeysDialog
|
||||
open={isApiKeysDialogOpen}
|
||||
onOpenChange={setIsApiKeysDialogOpen}
|
||||
apiKeys={apiKeys}
|
||||
isLoading={isLoading}
|
||||
onAddApiKey={async (keyData) => {
|
||||
await createApiKey({ ...keyData, client_id: clientId });
|
||||
loadApiKeys();
|
||||
}}
|
||||
onUpdateApiKey={async (id, keyData) => {
|
||||
await updateApiKey(id, keyData, clientId);
|
||||
loadApiKeys();
|
||||
}}
|
||||
onDeleteApiKey={async (id) => {
|
||||
await deleteApiKey(id, clientId);
|
||||
loadApiKeys();
|
||||
}}
|
||||
/>
|
||||
|
||||
<ShareAgentDialog
|
||||
open={isShareDialogOpen}
|
||||
onOpenChange={setIsShareDialogOpen}
|
||||
agent={agentToShare || ({} as Agent)}
|
||||
apiKey={sharedApiKey}
|
||||
/>
|
||||
|
||||
<ImportAgentDialog
|
||||
open={isImportDialogOpen}
|
||||
onOpenChange={setIsImportDialogOpen}
|
||||
onSuccess={loadAgents}
|
||||
clientId={clientId}
|
||||
folderId={selectedFolderId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
689
frontend/app/agents/workflows/Canva.tsx
Normal file
689
frontend/app/agents/workflows/Canva.tsx
Normal file
@@ -0,0 +1,689 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/Canva.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Delay node integration developed by: Victor Calazans │
|
||||
│ Creation date: May 13, 2025 |
|
||||
│ Delay implementation date: May 17, 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 {
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
|
||||
import {
|
||||
Controls,
|
||||
ReactFlow,
|
||||
addEdge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
type OnConnect,
|
||||
ConnectionMode,
|
||||
ConnectionLineType,
|
||||
useReactFlow,
|
||||
ProOptions,
|
||||
applyNodeChanges,
|
||||
NodeChange,
|
||||
OnNodesChange,
|
||||
MiniMap,
|
||||
Panel,
|
||||
Background,
|
||||
} from "@xyflow/react";
|
||||
import { useDnD } from "@/contexts/DnDContext";
|
||||
|
||||
import { Edit, X, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import "./canva.css";
|
||||
|
||||
import { getHelperLines } from "./utils";
|
||||
|
||||
import { NodePanel } from "./NodePanel";
|
||||
import ContextMenu from "./ContextMenu";
|
||||
import { initialEdges, edgeTypes } from "./edges";
|
||||
import HelperLines from "./HelperLines";
|
||||
import { initialNodes, nodeTypes } from "./nodes";
|
||||
import { AgentForm } from "./nodes/components/agent/AgentForm";
|
||||
import { ConditionForm } from "./nodes/components/condition/ConditionForm";
|
||||
import { Agent, WorkflowData } from "@/types/agent";
|
||||
import { updateAgent } from "@/services/agentService";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { MessageForm } from "./nodes/components/message/MessageForm";
|
||||
import { DelayForm } from "./nodes/components/delay/DelayForm";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const proOptions: ProOptions = { account: "paid-pro", hideAttribution: true };
|
||||
|
||||
const NodeFormWrapper = ({
|
||||
selectedNode,
|
||||
editingLabel,
|
||||
setEditingLabel,
|
||||
handleUpdateNode,
|
||||
setSelectedNode,
|
||||
children,
|
||||
}: {
|
||||
selectedNode: any;
|
||||
editingLabel: boolean;
|
||||
setEditingLabel: (value: boolean) => void;
|
||||
handleUpdateNode: (node: any) => void;
|
||||
setSelectedNode: (node: any) => void;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
// Handle ESC key to close the panel
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && !editingLabel) {
|
||||
setSelectedNode(null);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [setSelectedNode, editingLabel]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-shrink-0 sticky top-0 z-20 bg-neutral-800 shadow-md border-b border-neutral-700">
|
||||
<div className="p-4 text-center relative">
|
||||
<button
|
||||
className="absolute right-2 top-2 text-neutral-200 hover:text-white p-1 rounded-full hover:bg-neutral-700"
|
||||
onClick={() => setSelectedNode(null)}
|
||||
aria-label="Close panel"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
{!editingLabel ? (
|
||||
<div className="flex items-center justify-center text-xl font-bold text-neutral-200">
|
||||
<span>{selectedNode.data.label}</span>
|
||||
{selectedNode.type !== "start-node" && (
|
||||
<Edit
|
||||
size={16}
|
||||
className="ml-2 cursor-pointer hover:text-indigo-300"
|
||||
onClick={() => setEditingLabel(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={selectedNode.data.label}
|
||||
className="w-full p-2 text-center text-xl font-bold bg-neutral-800 text-neutral-200 border border-neutral-600 rounded"
|
||||
onChange={(e) => {
|
||||
handleUpdateNode({
|
||||
...selectedNode,
|
||||
data: {
|
||||
...selectedNode.data,
|
||||
label: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
setEditingLabel(false);
|
||||
}
|
||||
}}
|
||||
onBlur={() => setEditingLabel(false)}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0 overflow-hidden">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Canva = forwardRef(({ agent }: { agent: Agent | null }, ref) => {
|
||||
const { toast } = useToast();
|
||||
const [nodes, setNodes] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
const { type, setPointerEvents } = useDnD();
|
||||
const [menu, setMenu] = useState<any>(null);
|
||||
const localRef = useRef<any>(null);
|
||||
const [selectedNode, setSelectedNode] = useState<any>(null);
|
||||
const [activeExecutionNodeId, setActiveExecutionNodeId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
const [editingLabel, setEditingLabel] = useState(false);
|
||||
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
const [nodePanelOpen, setNodePanelOpen] = useState(false);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getFlowData: () => ({
|
||||
nodes,
|
||||
edges,
|
||||
}),
|
||||
setHasChanges,
|
||||
setActiveExecutionNodeId,
|
||||
}));
|
||||
|
||||
// Effect to clear the active node after a timeout
|
||||
useEffect(() => {
|
||||
if (activeExecutionNodeId) {
|
||||
const timer = setTimeout(() => {
|
||||
setActiveExecutionNodeId(null);
|
||||
}, 5000); // Increase to 5 seconds to give more time to visualize
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [activeExecutionNodeId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
agent?.config?.workflow &&
|
||||
agent.config.workflow.nodes.length > 0 &&
|
||||
agent.config.workflow.edges.length > 0
|
||||
) {
|
||||
setNodes(
|
||||
(agent.config.workflow.nodes as typeof initialNodes) || initialNodes
|
||||
);
|
||||
setEdges(
|
||||
(agent.config.workflow.edges as typeof initialEdges) || initialEdges
|
||||
);
|
||||
} else {
|
||||
setNodes(initialNodes);
|
||||
setEdges(initialEdges);
|
||||
}
|
||||
}, [agent, setNodes, setEdges]);
|
||||
|
||||
// Update nodes when the active node changes to add visual class
|
||||
useEffect(() => {
|
||||
if (nodes.length > 0) {
|
||||
setNodes((nds: any) =>
|
||||
nds.map((node: any) => {
|
||||
if (node.id === activeExecutionNodeId) {
|
||||
// Add a class to highlight the active node
|
||||
return {
|
||||
...node,
|
||||
className: "active-execution-node",
|
||||
data: {
|
||||
...node.data,
|
||||
isExecuting: true,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// Remove the highlight class
|
||||
const { isExecuting, ...restData } = node.data || {};
|
||||
return {
|
||||
...node,
|
||||
className: "",
|
||||
data: restData,
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [activeExecutionNodeId, setNodes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (agent?.config?.workflow) {
|
||||
const initialNodes = agent.config.workflow.nodes || [];
|
||||
const initialEdges = agent.config.workflow.edges || [];
|
||||
|
||||
if (
|
||||
JSON.stringify(nodes) !== JSON.stringify(initialNodes) ||
|
||||
JSON.stringify(edges) !== JSON.stringify(initialEdges)
|
||||
) {
|
||||
setHasChanges(true);
|
||||
} else {
|
||||
setHasChanges(false);
|
||||
}
|
||||
}
|
||||
}, [nodes, edges, agent]);
|
||||
|
||||
const [helperLineHorizontal, setHelperLineHorizontal] = useState<
|
||||
number | undefined
|
||||
>(undefined);
|
||||
const [helperLineVertical, setHelperLineVertical] = useState<
|
||||
number | undefined
|
||||
>(undefined);
|
||||
|
||||
const onConnect: OnConnect = useCallback(
|
||||
(connection) => {
|
||||
setEdges((currentEdges) => {
|
||||
if (connection.source === connection.target) {
|
||||
return currentEdges;
|
||||
}
|
||||
|
||||
return addEdge(connection, currentEdges);
|
||||
});
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const onConnectEnd = useCallback(
|
||||
(_event: any, connectionState: any) => {
|
||||
setPointerEvents("none");
|
||||
|
||||
if (connectionState.fromHandle?.type === "target") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connectionState.isValid) {
|
||||
// Since we're using NodePanel now, we don't need to do anything here
|
||||
// The panel will handle node creation through drag and drop
|
||||
}
|
||||
},
|
||||
[setPointerEvents]
|
||||
);
|
||||
|
||||
const onConnectStart = useCallback(() => {
|
||||
setPointerEvents("auto");
|
||||
}, [setPointerEvents]);
|
||||
|
||||
const customApplyNodeChanges = useCallback(
|
||||
(changes: NodeChange[], nodes: any): any => {
|
||||
// reset the helper lines (clear existing lines, if any)
|
||||
setHelperLineHorizontal(undefined);
|
||||
setHelperLineVertical(undefined);
|
||||
|
||||
// this will be true if it's a single node being dragged
|
||||
// inside we calculate the helper lines and snap position for the position where the node is being moved to
|
||||
if (
|
||||
changes.length === 1 &&
|
||||
changes[0].type === "position" &&
|
||||
changes[0].dragging &&
|
||||
changes[0].position
|
||||
) {
|
||||
const helperLines = getHelperLines(changes[0], nodes);
|
||||
|
||||
// if we have a helper line, we snap the node to the helper line position
|
||||
// this is being done by manipulating the node position inside the change object
|
||||
changes[0].position.x =
|
||||
helperLines.snapPosition.x ?? changes[0].position.x;
|
||||
changes[0].position.y =
|
||||
helperLines.snapPosition.y ?? changes[0].position.y;
|
||||
|
||||
// if helper lines are returned, we set them so that they can be displayed
|
||||
setHelperLineHorizontal(helperLines.horizontal);
|
||||
setHelperLineVertical(helperLines.vertical);
|
||||
}
|
||||
|
||||
return applyNodeChanges(changes, nodes);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const onNodesChange: OnNodesChange = useCallback(
|
||||
(changes) => {
|
||||
setNodes((nodes) => customApplyNodeChanges(changes, nodes));
|
||||
},
|
||||
[setNodes, customApplyNodeChanges]
|
||||
);
|
||||
|
||||
const getLabelFromNode = (type: string) => {
|
||||
const order = nodes.length;
|
||||
|
||||
switch (type) {
|
||||
case "start-node":
|
||||
return "Start";
|
||||
case "agent-node":
|
||||
return `Agent #${order}`;
|
||||
case "condition-node":
|
||||
return `Condition #${order}`;
|
||||
case "message-node":
|
||||
return `Message #${order}`;
|
||||
case "delay-node":
|
||||
return `Delay #${order}`;
|
||||
default:
|
||||
return "Node";
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddNode = useCallback(
|
||||
(type: any, node: any) => {
|
||||
const newNode: any = {
|
||||
id: String(Date.now()),
|
||||
type,
|
||||
position: node.position,
|
||||
data: {
|
||||
label: getLabelFromNode(type),
|
||||
},
|
||||
};
|
||||
|
||||
setNodes((nodes) => [...nodes, newNode]);
|
||||
|
||||
if (node.targetId) {
|
||||
const newEdge: any = {
|
||||
source: node.targetId,
|
||||
sourceHandle: node.handleId,
|
||||
target: newNode.id,
|
||||
type: "default",
|
||||
};
|
||||
|
||||
const newsEdges: any = [...edges, newEdge];
|
||||
|
||||
setEdges(newsEdges);
|
||||
}
|
||||
},
|
||||
[nodes, setNodes, edges, setEdges]
|
||||
);
|
||||
|
||||
const handleUpdateNode = useCallback(
|
||||
(node: any) => {
|
||||
setNodes((nodes) => {
|
||||
const index = nodes.findIndex((n) => n.id === node.id);
|
||||
if (index !== -1) {
|
||||
nodes[index] = node;
|
||||
}
|
||||
return [...nodes];
|
||||
});
|
||||
|
||||
if (selectedNode && selectedNode.id === node.id) {
|
||||
setSelectedNode(node);
|
||||
}
|
||||
},
|
||||
[setNodes, selectedNode]
|
||||
);
|
||||
|
||||
const handleDeleteEdge = useCallback(
|
||||
(id: any) => {
|
||||
setEdges((edges) => {
|
||||
const left = edges.filter((edge: any) => edge.id !== id);
|
||||
return left;
|
||||
});
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const onDragOver = useCallback((event: any) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
}, []);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(event: any) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
const newNode: any = {
|
||||
id: String(Date.now()),
|
||||
type,
|
||||
position,
|
||||
data: {
|
||||
label: getLabelFromNode(type),
|
||||
},
|
||||
};
|
||||
|
||||
setNodes((nodes) => [...nodes, newNode]);
|
||||
},
|
||||
[screenToFlowPosition, setNodes, type, getLabelFromNode]
|
||||
);
|
||||
|
||||
const onNodeContextMenu = useCallback(
|
||||
(event: any, node: any) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (node.id === "start-node") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!localRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const paneBounds = localRef.current.getBoundingClientRect();
|
||||
|
||||
const x = event.clientX - paneBounds.left;
|
||||
const y = event.clientY - paneBounds.top;
|
||||
|
||||
const menuWidth = 200;
|
||||
const menuHeight = 200;
|
||||
|
||||
const left = x + menuWidth > paneBounds.width ? undefined : x;
|
||||
const top = y + menuHeight > paneBounds.height ? undefined : y;
|
||||
const right =
|
||||
x + menuWidth > paneBounds.width ? paneBounds.width - x : undefined;
|
||||
const bottom =
|
||||
y + menuHeight > paneBounds.height ? paneBounds.height - y : undefined;
|
||||
|
||||
setMenu({
|
||||
id: node.id,
|
||||
left,
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
});
|
||||
},
|
||||
[setMenu]
|
||||
);
|
||||
|
||||
const onNodeClick = useCallback((event: any, node: any) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (node.type === "start-node") {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedNode(node);
|
||||
}, []);
|
||||
|
||||
const onPaneClick = useCallback(() => {
|
||||
setMenu(null);
|
||||
setSelectedNode(null);
|
||||
setNodePanelOpen(false);
|
||||
}, [setMenu, setSelectedNode]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full bg-[#121212]">
|
||||
<div
|
||||
style={{ position: "relative", height: "100%", width: "100%" }}
|
||||
ref={localRef}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodesChange={onNodesChange}
|
||||
edges={edges}
|
||||
edgeTypes={edgeTypes}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onConnectStart={onConnectStart}
|
||||
onConnectEnd={onConnectEnd}
|
||||
connectionMode={ConnectionMode.Strict}
|
||||
connectionLineType={ConnectionLineType.Bezier}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
onPaneClick={onPaneClick}
|
||||
onNodeClick={onNodeClick}
|
||||
onNodeContextMenu={onNodeContextMenu}
|
||||
colorMode="dark"
|
||||
minZoom={0.1}
|
||||
maxZoom={10}
|
||||
fitView={false}
|
||||
defaultViewport={{
|
||||
x: 0,
|
||||
y: 0,
|
||||
zoom: 1,
|
||||
}}
|
||||
elevateEdgesOnSelect
|
||||
elevateNodesOnSelect
|
||||
proOptions={proOptions}
|
||||
connectionLineStyle={{
|
||||
stroke: "gray",
|
||||
strokeWidth: 2,
|
||||
strokeDashoffset: 5,
|
||||
strokeDasharray: 5,
|
||||
}}
|
||||
defaultEdgeOptions={{
|
||||
type: "default",
|
||||
style: {
|
||||
strokeWidth: 3,
|
||||
},
|
||||
data: {
|
||||
handleDeleteEdge,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background color="#334155" gap={24} size={1.5} />
|
||||
<MiniMap
|
||||
className="bg-neutral-800/80 border border-neutral-700 rounded-lg shadow-lg"
|
||||
nodeColor={(node) => {
|
||||
switch (node.type) {
|
||||
case "start-node":
|
||||
return "#10b981";
|
||||
case "agent-node":
|
||||
return "#3b82f6";
|
||||
case "message-node":
|
||||
return "#f97316";
|
||||
case "condition-node":
|
||||
return "#3b82f6";
|
||||
case "delay-node":
|
||||
return "#eab308";
|
||||
default:
|
||||
return "#64748b";
|
||||
}
|
||||
}}
|
||||
maskColor="rgba(15, 23, 42, 0.6)"
|
||||
/>
|
||||
|
||||
<Controls
|
||||
showInteractive={true}
|
||||
showFitView={true}
|
||||
orientation="vertical"
|
||||
position="bottom-left"
|
||||
/>
|
||||
<HelperLines
|
||||
horizontal={helperLineHorizontal}
|
||||
vertical={helperLineVertical}
|
||||
/>
|
||||
{menu && <ContextMenu onClick={onPaneClick} {...menu} />}
|
||||
|
||||
{nodePanelOpen ? (
|
||||
<Panel position="top-right">
|
||||
<div className="flex items-start">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setNodePanelOpen(false)}
|
||||
className="mr-2 h-8 w-8 rounded-full bg-slate-800 border-slate-700 text-slate-400 hover:text-white hover:bg-slate-700"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
<NodePanel />
|
||||
</div>
|
||||
</Panel>
|
||||
) : (
|
||||
<Panel position="top-right">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setNodePanelOpen(true)}
|
||||
className="h-8 w-8 rounded-full bg-slate-800 border-slate-700 text-slate-400 hover:text-white hover:bg-slate-700"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
</Panel>
|
||||
)}
|
||||
</ReactFlow>
|
||||
|
||||
{/* Overlay when form is open on smaller screens */}
|
||||
{selectedNode && (
|
||||
<div
|
||||
className="md:hidden fixed inset-0 bg-black bg-opacity-50 z-[5] transition-opacity duration-300"
|
||||
onClick={() => setSelectedNode(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="absolute left-0 top-0 z-10 h-full w-[350px] bg-neutral-900 shadow-lg transition-all duration-300 ease-in-out border-r border-neutral-700 flex flex-col"
|
||||
style={{
|
||||
transform: selectedNode ? "translateX(0)" : "translateX(-100%)",
|
||||
opacity: selectedNode ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
{selectedNode ? (
|
||||
<NodeFormWrapper
|
||||
selectedNode={selectedNode}
|
||||
editingLabel={editingLabel}
|
||||
setEditingLabel={setEditingLabel}
|
||||
handleUpdateNode={handleUpdateNode}
|
||||
setSelectedNode={setSelectedNode}
|
||||
>
|
||||
{selectedNode.type === "agent-node" && (
|
||||
<AgentForm
|
||||
selectedNode={selectedNode}
|
||||
handleUpdateNode={handleUpdateNode}
|
||||
setEdges={setEdges}
|
||||
setIsOpen={() => {}}
|
||||
setSelectedNode={setSelectedNode}
|
||||
/>
|
||||
)}
|
||||
{selectedNode.type === "condition-node" && (
|
||||
<ConditionForm
|
||||
selectedNode={selectedNode}
|
||||
handleUpdateNode={handleUpdateNode}
|
||||
setEdges={setEdges}
|
||||
setIsOpen={() => {}}
|
||||
setSelectedNode={setSelectedNode}
|
||||
/>
|
||||
)}
|
||||
{selectedNode.type === "message-node" && (
|
||||
<MessageForm
|
||||
selectedNode={selectedNode}
|
||||
handleUpdateNode={handleUpdateNode}
|
||||
setEdges={setEdges}
|
||||
setIsOpen={() => {}}
|
||||
setSelectedNode={setSelectedNode}
|
||||
/>
|
||||
)}
|
||||
{selectedNode.type === "delay-node" && (
|
||||
<DelayForm
|
||||
selectedNode={selectedNode}
|
||||
handleUpdateNode={handleUpdateNode}
|
||||
setEdges={setEdges}
|
||||
setIsOpen={() => {}}
|
||||
setSelectedNode={setSelectedNode}
|
||||
/>
|
||||
)}
|
||||
</NodeFormWrapper>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Canva.displayName = "Canva";
|
||||
|
||||
export default Canva;
|
||||
118
frontend/app/agents/workflows/ContextMenu.tsx
Normal file
118
frontend/app/agents/workflows/ContextMenu.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/ContextMenu.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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
import { useReactFlow, Node, Edge } from "@xyflow/react";
|
||||
import { Copy, Trash2 } from "lucide-react";
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
interface ContextMenuProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
id: string;
|
||||
top?: number | string;
|
||||
left?: number | string;
|
||||
right?: number | string;
|
||||
bottom?: number | string;
|
||||
}
|
||||
|
||||
export default function ContextMenu({
|
||||
id,
|
||||
top,
|
||||
left,
|
||||
right,
|
||||
bottom,
|
||||
...props
|
||||
}: ContextMenuProps) {
|
||||
const { getNode, setNodes, addNodes, setEdges } = useReactFlow();
|
||||
|
||||
const duplicateNode = useCallback(() => {
|
||||
const node = getNode(id);
|
||||
|
||||
if (!node) {
|
||||
console.error(`Node with id ${id} not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const position = {
|
||||
x: node.position.x + 50,
|
||||
y: node.position.y + 50,
|
||||
};
|
||||
|
||||
addNodes({
|
||||
...node,
|
||||
id: `${node.id}-copy`,
|
||||
position,
|
||||
selected: false,
|
||||
dragging: false,
|
||||
});
|
||||
}, [id, getNode, addNodes]);
|
||||
|
||||
const deleteNode = useCallback(() => {
|
||||
setNodes((nodes: Node[]) => nodes.filter((node) => node.id !== id));
|
||||
setEdges((edges: Edge[]) =>
|
||||
edges.filter((edge) => edge.source !== id && edge.target !== id),
|
||||
);
|
||||
}, [id, setNodes, setEdges]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: top !== undefined ? `${top}px` : undefined,
|
||||
left: left !== undefined ? `${left}px` : undefined,
|
||||
right: right !== undefined ? `${right}px` : undefined,
|
||||
bottom: bottom !== undefined ? `${bottom}px` : undefined,
|
||||
zIndex: 10,
|
||||
}}
|
||||
className="context-menu rounded-md border p-3 shadow-lg border-neutral-700 bg-neutral-800"
|
||||
{...props}
|
||||
>
|
||||
<p className="mb-2 text-sm font-semibold text-neutral-200">
|
||||
Actions
|
||||
</p>
|
||||
<button
|
||||
onClick={duplicateNode}
|
||||
className="mb-1 flex w-full flex-row items-center rounded-md px-2 py-1 text-sm hover:bg-neutral-700"
|
||||
>
|
||||
<Copy
|
||||
size={16}
|
||||
className="mr-2 flex-shrink-0 text-blue-300"
|
||||
/>
|
||||
<span className="text-neutral-300">Duplicate</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={deleteNode}
|
||||
className="flex w-full flex-row items-center rounded-md px-2 py-1 text-sm hover:bg-neutral-700"
|
||||
>
|
||||
<Trash2
|
||||
size={16}
|
||||
className="mr-2 flex-shrink-0 text-red-300"
|
||||
/>
|
||||
<span className="text-neutral-300">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
98
frontend/app/agents/workflows/HelperLines.tsx
Normal file
98
frontend/app/agents/workflows/HelperLines.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/HelperLines.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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
import { ReactFlowState, useStore } from "@xyflow/react";
|
||||
import { CSSProperties, useEffect, useRef } from "react";
|
||||
|
||||
const canvasStyle: CSSProperties = {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
zIndex: 10,
|
||||
pointerEvents: "none",
|
||||
};
|
||||
|
||||
const storeSelector = (state: ReactFlowState) => ({
|
||||
width: state.width,
|
||||
height: state.height,
|
||||
transform: state.transform,
|
||||
});
|
||||
|
||||
export type HelperLinesProps = {
|
||||
horizontal?: number;
|
||||
vertical?: number;
|
||||
};
|
||||
|
||||
// a simple component to display the helper lines
|
||||
// it puts a canvas on top of the React Flow pane and draws the lines using the canvas API
|
||||
function HelperLinesRenderer({ horizontal, vertical }: HelperLinesProps) {
|
||||
const { width, height, transform } = useStore(storeSelector);
|
||||
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas?.getContext("2d");
|
||||
|
||||
if (!ctx || !canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dpi = window.devicePixelRatio;
|
||||
canvas.width = width * dpi;
|
||||
canvas.height = height * dpi;
|
||||
|
||||
ctx.scale(dpi, dpi);
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.strokeStyle = "#1d5ade";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.setLineDash([5, 5]);
|
||||
|
||||
if (typeof vertical === "number") {
|
||||
ctx.moveTo(vertical * transform[2] + transform[0], 0);
|
||||
ctx.lineTo(vertical * transform[2] + transform[0], height);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
if (typeof horizontal === "number") {
|
||||
ctx.moveTo(0, horizontal * transform[2] + transform[1]);
|
||||
ctx.lineTo(width, horizontal * transform[2] + transform[1]);
|
||||
ctx.stroke();
|
||||
}
|
||||
}, [width, height, transform, horizontal, vertical]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="react-flow__canvas"
|
||||
style={canvasStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default HelperLinesRenderer;
|
||||
273
frontend/app/agents/workflows/NodePanel.tsx
Normal file
273
frontend/app/agents/workflows/NodePanel.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
"use client";
|
||||
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
User,
|
||||
MessageSquare,
|
||||
Filter,
|
||||
Clock,
|
||||
Plus,
|
||||
MenuSquare,
|
||||
Layers,
|
||||
MoveRight,
|
||||
} from "lucide-react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useDnD } from "@/contexts/DnDContext";
|
||||
|
||||
export function NodePanel() {
|
||||
const [activeTab, setActiveTab] = useState("content");
|
||||
const { setType } = useDnD();
|
||||
|
||||
const nodeTypes = {
|
||||
content: [
|
||||
{
|
||||
id: "agent-node",
|
||||
name: "Agent",
|
||||
icon: User,
|
||||
color: "text-blue-400",
|
||||
bgColor: "bg-blue-950/40",
|
||||
borderColor: "border-blue-500/30",
|
||||
hoverColor: "group-hover:bg-blue-900/50",
|
||||
glowColor: "group-hover:shadow-blue-500/20",
|
||||
description: "Add an AI agent to process messages and execute tasks",
|
||||
},
|
||||
{
|
||||
id: "message-node",
|
||||
name: "Message",
|
||||
icon: MessageSquare,
|
||||
color: "text-orange-400",
|
||||
bgColor: "bg-orange-950/40",
|
||||
borderColor: "border-orange-500/30",
|
||||
hoverColor: "group-hover:bg-orange-900/50",
|
||||
glowColor: "group-hover:shadow-orange-500/20",
|
||||
description: "Send a message to users or other nodes in the workflow",
|
||||
},
|
||||
],
|
||||
logic: [
|
||||
{
|
||||
id: "condition-node",
|
||||
name: "Condition",
|
||||
icon: Filter,
|
||||
color: "text-purple-400",
|
||||
bgColor: "bg-purple-950/40",
|
||||
borderColor: "border-purple-500/30",
|
||||
hoverColor: "group-hover:bg-purple-900/50",
|
||||
glowColor: "group-hover:shadow-purple-500/20",
|
||||
description:
|
||||
"Create a decision point with multiple outcomes based on conditions",
|
||||
},
|
||||
{
|
||||
id: "delay-node",
|
||||
name: "Delay",
|
||||
icon: Clock,
|
||||
color: "text-yellow-400",
|
||||
bgColor: "bg-yellow-950/40",
|
||||
borderColor: "border-yellow-500/30",
|
||||
hoverColor: "group-hover:bg-yellow-900/50",
|
||||
glowColor: "group-hover:shadow-yellow-500/20",
|
||||
description: "Add a time delay between workflow operations",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const onDragStart = (event: React.DragEvent, nodeType: string) => {
|
||||
event.dataTransfer.setData("application/reactflow", nodeType);
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
setType(nodeType);
|
||||
};
|
||||
|
||||
const handleNodeAdd = (nodeType: string) => {
|
||||
setType(nodeType);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-slate-900/70 backdrop-blur-md border border-slate-700/50 rounded-xl shadow-xl w-[320px] transition-all duration-300 ease-in-out overflow-hidden">
|
||||
<div className="px-4 pt-4 pb-2 border-b border-slate-700/50">
|
||||
<div className="flex items-center gap-2 text-slate-200">
|
||||
<Layers className="h-5 w-5 text-indigo-400" />
|
||||
<h3 className="font-medium">Workflow Nodes</h3>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
Drag nodes to the canvas or click to add
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<div className="px-4 pt-3">
|
||||
<TabsList className="w-full bg-slate-800/50 grid grid-cols-2 p-1 rounded-lg">
|
||||
<TabsTrigger
|
||||
value="content"
|
||||
className={cn(
|
||||
"rounded-md text-xs font-medium transition-all",
|
||||
"data-[state=active]:bg-gradient-to-br data-[state=active]:from-blue-900/30 data-[state=active]:to-indigo-900/30",
|
||||
"data-[state=active]:text-blue-300 data-[state=active]:shadow-sm",
|
||||
"data-[state=inactive]:text-slate-400"
|
||||
)}
|
||||
>
|
||||
<MenuSquare className="h-3.5 w-3.5 mr-1.5" />
|
||||
Content
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="logic"
|
||||
className={cn(
|
||||
"rounded-md text-xs font-medium transition-all",
|
||||
"data-[state=active]:bg-gradient-to-br data-[state=active]:from-yellow-900/30 data-[state=active]:to-orange-900/30",
|
||||
"data-[state=active]:text-yellow-300 data-[state=active]:shadow-sm",
|
||||
"data-[state=inactive]:text-slate-400"
|
||||
)}
|
||||
>
|
||||
<Filter className="h-3.5 w-3.5 mr-1.5" />
|
||||
Logic
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="content" className="p-3 space-y-2 mt-0">
|
||||
{nodeTypes.content.map((node) => (
|
||||
<TooltipProvider key={node.id} delayDuration={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
draggable
|
||||
onDragStart={(event) => onDragStart(event, node.id)}
|
||||
className={cn(
|
||||
"group flex items-center gap-3 p-3.5 border rounded-lg cursor-grab transition-all duration-300",
|
||||
"backdrop-blur-sm hover:shadow-lg",
|
||||
node.borderColor,
|
||||
node.bgColor,
|
||||
node.glowColor
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center w-9 h-9 rounded-lg transition-all duration-300",
|
||||
"bg-slate-800/80 group-hover:scale-105",
|
||||
node.hoverColor
|
||||
)}
|
||||
>
|
||||
<node.icon className={cn("h-5 w-5", node.color)} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span
|
||||
className={cn("font-medium block text-sm", node.color)}
|
||||
>
|
||||
{node.name}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400 truncate block">
|
||||
{node.description}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => handleNodeAdd(node.id)}
|
||||
className={cn(
|
||||
"flex items-center justify-center h-7 w-7 rounded-md bg-slate-800/60 text-slate-400",
|
||||
"hover:bg-gradient-to-r hover:text-white transition-all",
|
||||
node.id === "agent-node"
|
||||
? "hover:from-blue-800 hover:to-blue-600"
|
||||
: "hover:from-orange-800 hover:to-orange-600"
|
||||
)}
|
||||
>
|
||||
<Plus size={16} />
|
||||
</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
className="bg-slate-900 border-slate-700 text-slate-200"
|
||||
>
|
||||
<div className="p-1 max-w-[200px]">
|
||||
<p className="font-medium text-sm">{node.name} Node</p>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
{node.description}
|
||||
</p>
|
||||
<div className="flex items-center mt-2 pt-2 border-t border-slate-700/50 text-xs text-slate-400">
|
||||
<MoveRight className="h-3 w-3 mr-1.5" />
|
||||
<span>Drag to canvas or click + to add</span>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
))}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="logic" className="p-3 space-y-2 mt-0">
|
||||
{nodeTypes.logic.map((node) => (
|
||||
<TooltipProvider key={node.id} delayDuration={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
draggable
|
||||
onDragStart={(event) => onDragStart(event, node.id)}
|
||||
className={cn(
|
||||
"group flex items-center gap-3 p-3.5 border rounded-lg cursor-grab transition-all duration-300",
|
||||
"backdrop-blur-sm hover:shadow-lg",
|
||||
node.borderColor,
|
||||
node.bgColor,
|
||||
node.glowColor
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center w-9 h-9 rounded-lg transition-all duration-300",
|
||||
"bg-slate-800/80 group-hover:scale-105",
|
||||
node.hoverColor
|
||||
)}
|
||||
>
|
||||
<node.icon className={cn("h-5 w-5", node.color)} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span
|
||||
className={cn("font-medium block text-sm", node.color)}
|
||||
>
|
||||
{node.name}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400 truncate block">
|
||||
{node.description}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => handleNodeAdd(node.id)}
|
||||
className={cn(
|
||||
"flex items-center justify-center h-7 w-7 rounded-md bg-slate-800/60 text-slate-400",
|
||||
"hover:bg-gradient-to-r hover:text-white transition-all",
|
||||
node.id === "condition-node"
|
||||
? "hover:from-purple-800 hover:to-purple-600"
|
||||
: "hover:from-yellow-800 hover:to-yellow-600"
|
||||
)}
|
||||
>
|
||||
<Plus size={16} />
|
||||
</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
className="bg-slate-900 border-slate-700 text-slate-200"
|
||||
>
|
||||
<div className="p-1 max-w-[200px]">
|
||||
<p className="font-medium text-sm">{node.name} Node</p>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
{node.description}
|
||||
</p>
|
||||
<div className="flex items-center mt-2 pt-2 border-t border-slate-700/50 text-xs text-slate-400">
|
||||
<MoveRight className="h-3 w-3 mr-1.5" />
|
||||
<span>Drag to canvas or click + to add</span>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
))}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
188
frontend/app/agents/workflows/canva.css
Normal file
188
frontend/app/agents/workflows/canva.css
Normal file
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/canva.css │
|
||||
│ 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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
.react-flow.dark {
|
||||
--xy-background-color-default: transparent;
|
||||
}
|
||||
|
||||
.react-flow__panel {
|
||||
box-shadow: none;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.react-flow__controls {
|
||||
background-color: rgba(30, 30, 30, 0.8);
|
||||
backdrop-filter: blur(4px);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(70, 70, 70, 0.5);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.react-flow__controls-button {
|
||||
padding: 6px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 4px 0;
|
||||
border: none !important;
|
||||
background-color: rgba(40, 40, 40, 0.8) !important;
|
||||
color: #a0a0a0 !important;
|
||||
border-radius: 6px !important;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.react-flow__controls-button:hover {
|
||||
background-color: rgba(60, 60, 60, 0.9) !important;
|
||||
color: #ffffff !important;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.react-flow__controls-button svg {
|
||||
fill: currentColor;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.react-flow__controls-button[data-action="zoom-in"] {
|
||||
border-bottom: 1px solid rgba(70, 70, 70, 0.5) !important;
|
||||
}
|
||||
|
||||
.react-flow__controls-button[data-action="zoom-out"] {
|
||||
border-bottom: 1px solid rgba(70, 70, 70, 0.5) !important;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for all browsers while maintaining scroll functionality */
|
||||
.scrollbar-hide {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari and Opera */
|
||||
width: 0;
|
||||
height: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Animated dashed edge for flow edges */
|
||||
.edge-dashed-animated {
|
||||
stroke-dasharray: 6;
|
||||
stroke-dashoffset: 0;
|
||||
animation: dash 0.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
to {
|
||||
stroke-dashoffset: -12;
|
||||
}
|
||||
}
|
||||
|
||||
/* Edges styling */
|
||||
.react-flow__edge-path {
|
||||
stroke: #10b981; /* Emerald color for edges */
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.react-flow__edge.selected .react-flow__edge-path {
|
||||
stroke: #3b82f6; /* Blue color for selected edges */
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
/* Node handle styling */
|
||||
.react-flow__handle {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #10b981;
|
||||
border: 2px solid #0d9488;
|
||||
}
|
||||
|
||||
.react-flow__handle:hover {
|
||||
background-color: #3b82f6;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
/* Nó em execução */
|
||||
.active-execution-node {
|
||||
animation: pulse-execution 1.5s infinite;
|
||||
filter: drop-shadow(0 0 15px rgba(5, 212, 114, 0.9));
|
||||
z-index: 10;
|
||||
transform: scale(1.03);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes pulse-execution {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(5, 212, 114, 0.9);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 15px rgba(5, 212, 114, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(5, 212, 114, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.react-flow__node[data-is-executing="true"] {
|
||||
filter: drop-shadow(0 0 15px rgba(5, 212, 114, 0.9));
|
||||
transform: scale(1.03);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.react-flow__node[data-is-executing="true"]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
left: -5px;
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
border: 2px solid #05d472;
|
||||
border-radius: 8px;
|
||||
animation: pulse-border 1.5s infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes pulse-border {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
70% {
|
||||
opacity: 0.3;
|
||||
transform: scale(1.03);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
119
frontend/app/agents/workflows/edges/DefaultEdge.tsx
Normal file
119
frontend/app/agents/workflows/edges/DefaultEdge.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/edges/DefaultEdge.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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
import {
|
||||
BaseEdge,
|
||||
EdgeLabelRenderer,
|
||||
EdgeProps,
|
||||
getSmoothStepPath,
|
||||
useReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import { Trash2 } from "lucide-react";
|
||||
|
||||
export default function DefaultEdge({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
style = {},
|
||||
selected,
|
||||
}: EdgeProps) {
|
||||
const { setEdges } = useReactFlow();
|
||||
|
||||
const [edgePath, labelX, labelY] = getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
borderRadius: 15,
|
||||
});
|
||||
|
||||
const onEdgeClick = () => {
|
||||
console.log("onEdgeClick", id);
|
||||
setEdges((edges) => edges.filter((edge) => edge.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<svg>
|
||||
<defs>
|
||||
<marker
|
||||
id="arrowhead"
|
||||
viewBox="0 0 10 16"
|
||||
refX="12"
|
||||
refY="8"
|
||||
markerWidth="4"
|
||||
markerHeight="5"
|
||||
orient="auto"
|
||||
>
|
||||
<path d="M 0 0 L 10 8 L 0 16 z" fill="gray" />
|
||||
</marker>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<BaseEdge
|
||||
id={id}
|
||||
path={edgePath}
|
||||
className="edge-dashed-animated"
|
||||
style={{
|
||||
...style,
|
||||
stroke: '#10B981',
|
||||
strokeWidth: 3,
|
||||
}}
|
||||
markerEnd="url(#arrowhead)"
|
||||
/>
|
||||
|
||||
{selected && (
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||
fontSize: 12,
|
||||
pointerEvents: "all",
|
||||
zIndex: 1000,
|
||||
}}
|
||||
className="nodrag nopan"
|
||||
>
|
||||
<button
|
||||
className="rounded-full bg-white p-1 shadow-md"
|
||||
onClick={onEdgeClick}
|
||||
>
|
||||
<Trash2 className="text-red-500" size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
41
frontend/app/agents/workflows/edges/index.ts
Normal file
41
frontend/app/agents/workflows/edges/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/edges/index.ts │
|
||||
│ 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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
import type { Edge, EdgeTypes } from "@xyflow/react";
|
||||
|
||||
import DefaultEdge from "./DefaultEdge";
|
||||
|
||||
export const initialEdges = [
|
||||
// { id: "a->c", source: "a", target: "c", animated: true },
|
||||
// { id: "b->d", source: "b", target: "d" },
|
||||
// { id: "c->d", source: "c", target: "d", animated: true },
|
||||
] satisfies Edge[];
|
||||
|
||||
export const edgeTypes = {
|
||||
default: DefaultEdge,
|
||||
} satisfies EdgeTypes;
|
||||
166
frontend/app/agents/workflows/nodes/BaseNode.tsx
Normal file
166
frontend/app/agents/workflows/nodes/BaseNode.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/BaseNode.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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
import { Handle, Position } from "@xyflow/react";
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useDnD } from "@/contexts/DnDContext";
|
||||
|
||||
export function BaseNode({
|
||||
selected,
|
||||
hasTarget,
|
||||
children,
|
||||
borderColor,
|
||||
isExecuting
|
||||
}: {
|
||||
selected: boolean;
|
||||
hasTarget: boolean;
|
||||
children: React.ReactNode;
|
||||
borderColor: string;
|
||||
isExecuting?: boolean;
|
||||
}) {
|
||||
const { pointerEvents } = useDnD();
|
||||
|
||||
// Border and background color mapping
|
||||
const colorStyles = {
|
||||
blue: {
|
||||
border: "border-blue-700/70 hover:border-blue-500",
|
||||
gradient: "bg-gradient-to-br from-blue-950/40 to-neutral-900/90",
|
||||
glow: "shadow-[0_0_15px_rgba(59,130,246,0.15)]",
|
||||
selectedGlow: "shadow-[0_0_25px_rgba(59,130,246,0.3)]"
|
||||
},
|
||||
orange: {
|
||||
border: "border-orange-700/70 hover:border-orange-500",
|
||||
gradient: "bg-gradient-to-br from-orange-950/40 to-neutral-900/90",
|
||||
glow: "shadow-[0_0_15px_rgba(249,115,22,0.15)]",
|
||||
selectedGlow: "shadow-[0_0_25px_rgba(249,115,22,0.3)]"
|
||||
},
|
||||
green: {
|
||||
border: "border-green-700/70 hover:border-green-500",
|
||||
gradient: "bg-gradient-to-br from-green-950/40 to-neutral-900/90",
|
||||
glow: "shadow-[0_0_15px_rgba(34,197,94,0.15)]",
|
||||
selectedGlow: "shadow-[0_0_25px_rgba(34,197,94,0.3)]"
|
||||
},
|
||||
red: {
|
||||
border: "border-red-700/70 hover:border-red-500",
|
||||
gradient: "bg-gradient-to-br from-red-950/40 to-neutral-900/90",
|
||||
glow: "shadow-[0_0_15px_rgba(239,68,68,0.15)]",
|
||||
selectedGlow: "shadow-[0_0_25px_rgba(239,68,68,0.3)]"
|
||||
},
|
||||
yellow: {
|
||||
border: "border-yellow-700/70 hover:border-yellow-500",
|
||||
gradient: "bg-gradient-to-br from-yellow-950/40 to-neutral-900/90",
|
||||
glow: "shadow-[0_0_15px_rgba(234,179,8,0.15)]",
|
||||
selectedGlow: "shadow-[0_0_25px_rgba(234,179,8,0.3)]"
|
||||
},
|
||||
purple: {
|
||||
border: "border-purple-700/70 hover:border-purple-500",
|
||||
gradient: "bg-gradient-to-br from-purple-950/40 to-neutral-900/90",
|
||||
glow: "shadow-[0_0_15px_rgba(168,85,247,0.15)]",
|
||||
selectedGlow: "shadow-[0_0_25px_rgba(168,85,247,0.3)]"
|
||||
},
|
||||
indigo: {
|
||||
border: "border-indigo-700/70 hover:border-indigo-500",
|
||||
gradient: "bg-gradient-to-br from-indigo-950/40 to-neutral-900/90",
|
||||
glow: "shadow-[0_0_15px_rgba(99,102,241,0.15)]",
|
||||
selectedGlow: "shadow-[0_0_25px_rgba(99,102,241,0.3)]"
|
||||
},
|
||||
pink: {
|
||||
border: "border-pink-700/70 hover:border-pink-500",
|
||||
gradient: "bg-gradient-to-br from-pink-950/40 to-neutral-900/90",
|
||||
glow: "shadow-[0_0_15px_rgba(236,72,153,0.15)]",
|
||||
selectedGlow: "shadow-[0_0_25px_rgba(236,72,153,0.3)]"
|
||||
},
|
||||
emerald: {
|
||||
border: "border-emerald-700/70 hover:border-emerald-500",
|
||||
gradient: "bg-gradient-to-br from-emerald-950/40 to-neutral-900/90",
|
||||
glow: "shadow-[0_0_15px_rgba(16,185,129,0.15)]",
|
||||
selectedGlow: "shadow-[0_0_25px_rgba(16,185,129,0.3)]"
|
||||
},
|
||||
slate: {
|
||||
border: "border-slate-700/70 hover:border-slate-500",
|
||||
gradient: "bg-gradient-to-br from-slate-800/40 to-neutral-900/90",
|
||||
glow: "shadow-[0_0_15px_rgba(100,116,139,0.15)]",
|
||||
selectedGlow: "shadow-[0_0_25px_rgba(100,116,139,0.3)]"
|
||||
},
|
||||
};
|
||||
|
||||
// Default to blue if color not in mapping
|
||||
const colorStyle = colorStyles[borderColor as keyof typeof colorStyles] || colorStyles.blue;
|
||||
|
||||
// Selected styles
|
||||
const selectedStyle = {
|
||||
border: "border-green-500/90",
|
||||
glow: colorStyle.selectedGlow
|
||||
};
|
||||
|
||||
// Executing styles
|
||||
const executingStyle = {
|
||||
border: "border-emerald-500",
|
||||
glow: "shadow-[0_0_25px_rgba(5,212,114,0.5)]"
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"relative z-0 w-[350px] rounded-2xl p-4 border-2 backdrop-blur-sm transition-all duration-300",
|
||||
"shadow-lg hover:shadow-xl",
|
||||
isExecuting ? executingStyle.glow : selected ? selectedStyle.glow : colorStyle.glow,
|
||||
isExecuting ? executingStyle.border : selected ? selectedStyle.border : colorStyle.border,
|
||||
colorStyle.gradient,
|
||||
isExecuting && "active-execution-node"
|
||||
)}
|
||||
style={{
|
||||
backdropFilter: "blur(12px)",
|
||||
}}
|
||||
data-is-executing={isExecuting ? "true" : "false"}
|
||||
>
|
||||
{hasTarget && (
|
||||
<Handle
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "50%",
|
||||
width: "100%",
|
||||
borderRadius: "15px",
|
||||
height: "100%",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
pointerEvents: pointerEvents === "none" ? "none" : "auto",
|
||||
}}
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
/>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/agent/AgentChatMessageList.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 14, 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 React, { useEffect, useRef } from "react";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { ChevronDown, ChevronRight, Copy, Check } from "lucide-react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { useState } from "react";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { InlineDataAttachments } from "@/app/chat/components/InlineDataAttachments";
|
||||
|
||||
interface FunctionMessageContent {
|
||||
title: string;
|
||||
content: string;
|
||||
author?: string;
|
||||
}
|
||||
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
content: {
|
||||
parts: any[];
|
||||
role: string;
|
||||
};
|
||||
author: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface AgentChatMessageListProps {
|
||||
messages: ChatMessage[];
|
||||
agent: Agent;
|
||||
expandedFunctions: Record<string, boolean>;
|
||||
toggleFunctionExpansion: (messageId: string) => void;
|
||||
getMessageText: (message: ChatMessage) => string | FunctionMessageContent;
|
||||
containsMarkdown: (text: string) => boolean;
|
||||
}
|
||||
|
||||
export function AgentChatMessageList({
|
||||
messages,
|
||||
agent,
|
||||
expandedFunctions,
|
||||
toggleFunctionExpansion,
|
||||
getMessageText,
|
||||
containsMarkdown,
|
||||
}: AgentChatMessageListProps) {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Scroll to bottom whenever messages change
|
||||
useEffect(() => {
|
||||
if (messagesEndRef.current) {
|
||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{messages.map((message) => {
|
||||
const messageContent = getMessageText(message);
|
||||
const isExpanded = expandedFunctions[message.id] || false;
|
||||
const isUser = message.author === "user";
|
||||
const agentColor = "bg-emerald-400";
|
||||
const hasFunctionCall = message.content.parts.some(
|
||||
(part) => part.functionCall || part.function_call
|
||||
);
|
||||
const hasFunctionResponse = message.content.parts.some(
|
||||
(part) => part.functionResponse || part.function_response
|
||||
);
|
||||
const isFunctionMessage = hasFunctionCall || hasFunctionResponse;
|
||||
const isTaskExecutor = typeof messageContent === "object" &&
|
||||
"author" in messageContent &&
|
||||
typeof messageContent.author === "string" &&
|
||||
messageContent.author.endsWith("- Task executor");
|
||||
|
||||
const inlineDataParts = message.content.parts.filter(part => part.inline_data);
|
||||
const hasInlineData = inlineDataParts.length > 0;
|
||||
|
||||
const isWorkflowNode = message.author && message.author.startsWith('workflow-node:');
|
||||
const nodeId = isWorkflowNode ? message.author.split(':')[1] : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
className="flex w-full"
|
||||
style={{
|
||||
justifyContent: isUser ? "flex-end" : "flex-start"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex gap-3 max-w-[90%]"
|
||||
style={{
|
||||
flexDirection: isUser ? "row-reverse" : "row"
|
||||
}}
|
||||
>
|
||||
<Avatar className={isUser ? "bg-[#333]" : agentColor}>
|
||||
<AvatarFallback>
|
||||
{isUser ? "U" : agent.name[0] || "A"}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div
|
||||
className={`rounded-lg p-3 ${isFunctionMessage || isTaskExecutor
|
||||
? "bg-[#333] text-emerald-400 font-mono text-sm"
|
||||
: isUser
|
||||
? "bg-emerald-400 text-black"
|
||||
: "bg-[#222] text-white"
|
||||
} overflow-hidden relative group`}
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
maxWidth: "calc(100% - 3rem)",
|
||||
width: "100%",
|
||||
...(isWorkflowNode ? {
|
||||
borderLeft: '3px solid #05d472',
|
||||
boxShadow: '0 0 10px rgba(5, 212, 114, 0.2)'
|
||||
} : {})
|
||||
}}
|
||||
>
|
||||
{isWorkflowNode && (
|
||||
<div className="text-xs text-emerald-500 mb-1 flex items-center space-x-1 bg-emerald-950/30 p-1 rounded">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-3 w-3 mr-1">
|
||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
||||
</svg>
|
||||
<span>Node {nodeId} is running</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isFunctionMessage || isTaskExecutor ? (
|
||||
<div className="w-full">
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer hover:bg-[#444] rounded px-1 py-0.5 transition-colors"
|
||||
onClick={() => toggleFunctionExpansion(message.id)}
|
||||
>
|
||||
{typeof messageContent === "object" &&
|
||||
"title" in messageContent && (
|
||||
<>
|
||||
<div className="flex-1 font-semibold">
|
||||
{(messageContent as FunctionMessageContent).title}
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-5 h-5 text-emerald-400">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isTaskExecutor && (
|
||||
<>
|
||||
<div className="flex-1 font-semibold">
|
||||
Task Execution
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-5 h-5 text-emerald-400">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="mt-2 pt-2 border-t border-[#555]">
|
||||
{typeof messageContent === "object" &&
|
||||
"content" in messageContent && (
|
||||
<div className="max-w-full overflow-x-auto">
|
||||
<pre className="whitespace-pre-wrap text-xs max-w-full" style={{
|
||||
wordWrap: "break-word",
|
||||
maxWidth: "100%",
|
||||
wordBreak: "break-all"
|
||||
}}>
|
||||
{(messageContent as FunctionMessageContent).content}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="markdown-content break-words max-w-full overflow-x-auto">
|
||||
{typeof messageContent === "object" &&
|
||||
"author" in messageContent &&
|
||||
messageContent.author !== "user" &&
|
||||
!isTaskExecutor && (
|
||||
<div className="text-xs text-neutral-400 mb-1">
|
||||
{messageContent.author}
|
||||
</div>
|
||||
)}
|
||||
{((typeof messageContent === "string" &&
|
||||
containsMarkdown(messageContent)) ||
|
||||
(typeof messageContent === "object" &&
|
||||
"content" in messageContent &&
|
||||
typeof messageContent.content === "string" &&
|
||||
containsMarkdown(messageContent.content))) &&
|
||||
!isTaskExecutor ? (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
h1: ({ ...props }) => (
|
||||
<h1 className="text-xl font-bold my-4" {...props} />
|
||||
),
|
||||
h2: ({ ...props }) => (
|
||||
<h2 className="text-lg font-bold my-3" {...props} />
|
||||
),
|
||||
h3: ({ ...props }) => (
|
||||
<h3 className="text-base font-bold my-2" {...props} />
|
||||
),
|
||||
h4: ({ ...props }) => (
|
||||
<h4 className="font-semibold my-2" {...props} />
|
||||
),
|
||||
p: ({ ...props }) => <p className="mb-3" {...props} />,
|
||||
ul: ({ ...props }) => (
|
||||
<ul
|
||||
className="list-disc pl-6 mb-3 space-y-1"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ol: ({ ...props }) => (
|
||||
<ol
|
||||
className="list-decimal pl-6 mb-3 space-y-1"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
li: ({ ...props }) => <li className="mb-1" {...props} />,
|
||||
a: ({ ...props }) => (
|
||||
<a
|
||||
className="text-emerald-400 underline hover:opacity-80 transition-opacity"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
blockquote: ({ ...props }) => (
|
||||
<blockquote
|
||||
className="border-l-4 border-[#444] pl-4 py-1 italic my-3 text-neutral-300"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
code: ({ className, children, ...props }: any) => {
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
const isInline = !match && typeof children === "string" && !children.includes("\n");
|
||||
|
||||
if (isInline) {
|
||||
return (
|
||||
<code
|
||||
className="bg-[#333] px-1.5 py-0.5 rounded text-emerald-400 text-sm font-mono"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-3 relative">
|
||||
<div className="bg-[#1a1a1a] rounded-t-md border-b border-[#333] p-2 text-xs text-neutral-400">
|
||||
<span>{match?.[1] || "Code"}</span>
|
||||
</div>
|
||||
<pre className="bg-[#1a1a1a] p-3 rounded-b-md overflow-x-auto whitespace-pre text-sm">
|
||||
<code {...props}>{children}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
table: ({ ...props }) => (
|
||||
<div className="overflow-x-auto my-3">
|
||||
<table
|
||||
className="min-w-full border border-[#333] rounded"
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
thead: ({ ...props }) => (
|
||||
<thead className="bg-[#1a1a1a]" {...props} />
|
||||
),
|
||||
tbody: ({ ...props }) => <tbody {...props} />,
|
||||
tr: ({ ...props }) => (
|
||||
<tr
|
||||
className="border-b border-[#333] last:border-0"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
th: ({ ...props }) => (
|
||||
<th
|
||||
className="px-4 py-2 text-left text-xs font-semibold text-neutral-300"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
td: ({ ...props }) => (
|
||||
<td className="px-4 py-2 text-sm" {...props} />
|
||||
),
|
||||
}}
|
||||
>
|
||||
{typeof messageContent === "string"
|
||||
? messageContent
|
||||
: messageContent.content}
|
||||
</ReactMarkdown>
|
||||
) : (
|
||||
<div className="whitespace-pre-wrap">
|
||||
{typeof messageContent === "string"
|
||||
? messageContent
|
||||
: messageContent.content}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasInlineData && (
|
||||
<InlineDataAttachments parts={inlineDataParts} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* Empty div at the end for auto-scrolling */}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,631 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/agent/AgentForm.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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* eslint-disable jsx-a11y/alt-text */
|
||||
import { useEdges, useNodes } from "@xyflow/react";
|
||||
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { listAgents, listFolders, Folder, getAgent } from "@/services/agentService";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { User, Loader2, Search, FolderIcon, Trash2, Play, MessageSquare, PlayIcon, Plus, X } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { AgentForm as GlobalAgentForm } from "@/app/agents/forms/AgentForm";
|
||||
import { ApiKey, listApiKeys } from "@/services/agentService";
|
||||
import { listMCPServers } from "@/services/mcpServerService";
|
||||
import { availableModels } from "@/types/aiModels";
|
||||
import { MCPServer } from "@/types/mcpServer";
|
||||
import { AgentTestChatModal } from "./AgentTestChatModal";
|
||||
import { sanitizeAgentName, escapePromptBraces } from "@/lib/utils";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const user = typeof window !== "undefined" ? JSON.parse(localStorage.getItem("user") || '{}') : {};
|
||||
const clientId: string = user?.client_id ? String(user.client_id) : "";
|
||||
|
||||
const agentListStyles = {
|
||||
scrollbarWidth: 'none', /* Firefox */
|
||||
msOverflowStyle: 'none', /* IE and Edge */
|
||||
'::-webkit-scrollbar': {
|
||||
display: 'none' /* Chrome, Safari and Opera */
|
||||
}
|
||||
};
|
||||
|
||||
export function AgentForm({ selectedNode, handleUpdateNode, setEdges, setIsOpen, setSelectedNode }: {
|
||||
selectedNode: any;
|
||||
handleUpdateNode: any;
|
||||
setEdges: any;
|
||||
setIsOpen: any;
|
||||
setSelectedNode: any;
|
||||
}) {
|
||||
const [node, setNode] = useState(selectedNode);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadingFolders, setLoadingFolders] = useState(true);
|
||||
const [loadingCurrentAgent, setLoadingCurrentAgent] = useState(false);
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [allAgents, setAllAgents] = useState<Agent[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [folders, setFolders] = useState<Folder[]>([]);
|
||||
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null);
|
||||
const [selectedAgentType, setSelectedAgentType] = useState<string | null>(null);
|
||||
const [agentTypes, setAgentTypes] = useState<string[]>([]);
|
||||
const [agentFolderId, setAgentFolderId] = useState<string | null>(null);
|
||||
const edges = useEdges();
|
||||
const nodes = useNodes();
|
||||
const [isAgentDialogOpen, setIsAgentDialogOpen] = useState(false);
|
||||
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
|
||||
const [availableMCPs, setAvailableMCPs] = useState<MCPServer[]>([]);
|
||||
const [newAgent, setNewAgent] = useState<Partial<Agent>>({
|
||||
client_id: clientId || "",
|
||||
name: "",
|
||||
description: "",
|
||||
type: "llm",
|
||||
model: "openai/gpt-4.1-nano",
|
||||
instruction: "",
|
||||
api_key_id: "",
|
||||
config: {
|
||||
tools: [],
|
||||
mcp_servers: [],
|
||||
custom_mcp_servers: [],
|
||||
custom_tools: { http_tools: [] },
|
||||
sub_agents: [],
|
||||
agent_tools: [],
|
||||
},
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isTestModalOpen, setIsTestModalOpen] = useState(false);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
// Access the canvas reference from localStorage
|
||||
const canvasRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// When the component is mounted, check if there is a canvas reference in the global context
|
||||
if (typeof window !== "undefined") {
|
||||
const workflowsPage = document.querySelector('[data-workflow-page="true"]');
|
||||
if (workflowsPage) {
|
||||
// If we are on the workflows page, try to access the canvas ref
|
||||
const canvasElement = workflowsPage.querySelector('[data-canvas-ref="true"]');
|
||||
if (canvasElement && (canvasElement as any).__reactRef) {
|
||||
canvasRef.current = (canvasElement as any).__reactRef.current;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const connectedNode = useMemo(() => {
|
||||
const edge = edges.find((edge: any) => edge.source === selectedNode.id);
|
||||
if (!edge) return null;
|
||||
const node = nodes.find((node: any) => node.id === edge.target);
|
||||
return node || null;
|
||||
}, [edges, nodes, selectedNode.id]);
|
||||
|
||||
const currentAgent = typeof window !== "undefined" ?
|
||||
JSON.parse(localStorage.getItem("current_workflow_agent") || '{}') : {};
|
||||
const currentAgentId = currentAgent?.id;
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedNode) {
|
||||
setNode(selectedNode);
|
||||
}
|
||||
}, [selectedNode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clientId) return;
|
||||
setLoadingFolders(true);
|
||||
listFolders(clientId)
|
||||
.then((res) => {
|
||||
setFolders(res.data);
|
||||
})
|
||||
.catch((error) => console.error("Error loading folders:", error))
|
||||
.finally(() => setLoadingFolders(false));
|
||||
}, [clientId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentAgentId || !clientId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingCurrentAgent(true);
|
||||
|
||||
getAgent(currentAgentId, clientId)
|
||||
.then((res) => {
|
||||
const agent = res.data;
|
||||
if (agent.folder_id) {
|
||||
setAgentFolderId(agent.folder_id);
|
||||
setSelectedFolderId(agent.folder_id);
|
||||
}
|
||||
})
|
||||
.catch((error) => console.error("Error loading current agent:", error))
|
||||
.finally(() => setLoadingCurrentAgent(false));
|
||||
}, [currentAgentId, clientId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clientId) return;
|
||||
|
||||
if (loadingFolders || loadingCurrentAgent) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
listAgents(clientId, 0, 100, selectedFolderId || undefined)
|
||||
.then((res) => {
|
||||
const filteredAgents = res.data.filter((agent: Agent) => agent.id !== currentAgentId);
|
||||
setAllAgents(filteredAgents);
|
||||
setAgents(filteredAgents);
|
||||
|
||||
// Extract unique agent types
|
||||
const types = [...new Set(filteredAgents.map(agent => agent.type))].filter(Boolean);
|
||||
setAgentTypes(types);
|
||||
})
|
||||
.catch((error) => console.error("Error loading agents:", error))
|
||||
.finally(() => setLoading(false));
|
||||
}, [clientId, currentAgentId, selectedFolderId, loadingFolders, loadingCurrentAgent]);
|
||||
|
||||
useEffect(() => {
|
||||
// Apply all filters: search, folder, and type
|
||||
let filtered = allAgents;
|
||||
|
||||
// Search filter is applied in a separate effect
|
||||
if (searchQuery.trim() !== "") {
|
||||
filtered = filtered.filter((agent) =>
|
||||
agent.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
agent.description?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
// Apply agent type filter
|
||||
if (selectedAgentType) {
|
||||
filtered = filtered.filter(agent => agent.type === selectedAgentType);
|
||||
}
|
||||
|
||||
setAgents(filtered);
|
||||
}, [searchQuery, selectedAgentType, allAgents]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clientId) return;
|
||||
listApiKeys(clientId).then((res) => setApiKeys(res.data));
|
||||
listMCPServers().then((res) => setAvailableMCPs(res.data));
|
||||
}, [clientId]);
|
||||
|
||||
const handleDeleteEdge = useCallback(() => {
|
||||
const id = edges.find((edge: any) => edge.source === selectedNode.id)?.id;
|
||||
setEdges((edges: any) => {
|
||||
const left = edges.filter((edge: any) => edge.id !== id);
|
||||
return left;
|
||||
});
|
||||
}, [nodes, edges, selectedNode, setEdges]);
|
||||
|
||||
const handleSelectAgent = (agent: Agent) => {
|
||||
const updatedNode = {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
agent,
|
||||
},
|
||||
};
|
||||
setNode(updatedNode);
|
||||
handleUpdateNode(updatedNode);
|
||||
};
|
||||
|
||||
const getAgentTypeName = (type: string) => {
|
||||
const agentTypes: Record<string, string> = {
|
||||
llm: "LLM Agent",
|
||||
a2a: "A2A Agent",
|
||||
sequential: "Sequential Agent",
|
||||
parallel: "Parallel Agent",
|
||||
loop: "Loop Agent",
|
||||
workflow: "Workflow Agent",
|
||||
task: "Task Agent",
|
||||
};
|
||||
return agentTypes[type] || type;
|
||||
};
|
||||
|
||||
const handleOpenAgentDialog = () => {
|
||||
setNewAgent({
|
||||
client_id: clientId || "",
|
||||
name: "",
|
||||
description: "",
|
||||
type: "llm",
|
||||
model: "openai/gpt-4.1-nano",
|
||||
instruction: "",
|
||||
api_key_id: "",
|
||||
config: {
|
||||
tools: [],
|
||||
mcp_servers: [],
|
||||
custom_mcp_servers: [],
|
||||
custom_tools: { http_tools: [] },
|
||||
sub_agents: [],
|
||||
agent_tools: [],
|
||||
},
|
||||
folder_id: selectedFolderId || undefined,
|
||||
});
|
||||
setIsAgentDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveAgent = async (agentData: Partial<Agent>) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const sanitizedData = {
|
||||
...agentData,
|
||||
client_id: clientId,
|
||||
name: agentData.name ? sanitizeAgentName(agentData.name) : agentData.name,
|
||||
instruction: agentData.instruction ? escapePromptBraces(agentData.instruction) : agentData.instruction
|
||||
};
|
||||
|
||||
if (isEditMode && node.data.agent?.id) {
|
||||
// Update existing agent
|
||||
const { updateAgent } = await import("@/services/agentService");
|
||||
const updated = await updateAgent(node.data.agent.id, sanitizedData as any);
|
||||
|
||||
// Refresh the agent list
|
||||
const res = await listAgents(clientId, 0, 100, selectedFolderId || undefined);
|
||||
const filteredAgents = res.data.filter((agent: Agent) => agent.id !== currentAgentId);
|
||||
setAllAgents(filteredAgents);
|
||||
setAgents(filteredAgents);
|
||||
|
||||
if (updated.data) {
|
||||
handleSelectAgent(updated.data);
|
||||
}
|
||||
} else {
|
||||
// Create new agent
|
||||
const { createAgent } = await import("@/services/agentService");
|
||||
const created = await createAgent(sanitizedData as any);
|
||||
|
||||
const res = await listAgents(clientId, 0, 100, selectedFolderId || undefined);
|
||||
const filteredAgents = res.data.filter((agent: Agent) => agent.id !== currentAgentId);
|
||||
setAllAgents(filteredAgents);
|
||||
setAgents(filteredAgents);
|
||||
|
||||
if (created.data) {
|
||||
handleSelectAgent(created.data);
|
||||
}
|
||||
}
|
||||
|
||||
setIsAgentDialogOpen(false);
|
||||
setIsEditMode(false);
|
||||
} catch (e) {
|
||||
console.error("Error saving agent:", e);
|
||||
setIsAgentDialogOpen(false);
|
||||
setIsEditMode(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFolderChange = (value: string) => {
|
||||
setSelectedFolderId(value === "all" ? null : value);
|
||||
};
|
||||
|
||||
const handleAgentTypeChange = (value: string) => {
|
||||
setSelectedAgentType(value === "all" ? null : value);
|
||||
};
|
||||
|
||||
const getFolderNameById = (id: string) => {
|
||||
const folder = folders.find((f) => f.id === id);
|
||||
return folder?.name || id;
|
||||
};
|
||||
|
||||
const handleEditAgent = () => {
|
||||
if (!node.data.agent) return;
|
||||
|
||||
setNewAgent({
|
||||
...node.data.agent,
|
||||
client_id: clientId || "",
|
||||
});
|
||||
|
||||
setIsEditMode(true);
|
||||
setIsAgentDialogOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isAgentDialogOpen && (
|
||||
<GlobalAgentForm
|
||||
open={isAgentDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setIsAgentDialogOpen(open);
|
||||
if (!open) setIsEditMode(false);
|
||||
}}
|
||||
initialValues={newAgent}
|
||||
apiKeys={apiKeys}
|
||||
availableModels={availableModels}
|
||||
availableMCPs={availableMCPs}
|
||||
agents={allAgents}
|
||||
onOpenApiKeysDialog={() => {}}
|
||||
onOpenMCPDialog={() => {}}
|
||||
onOpenCustomMCPDialog={() => {}}
|
||||
onSave={handleSaveAgent}
|
||||
isLoading={isLoading}
|
||||
getAgentNameById={(id) => allAgents.find((a) => a.id === id)?.name || id}
|
||||
clientId={clientId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Agent Test Chat Modal - moved outside of nested divs to render properly */}
|
||||
{isTestModalOpen && node.data.agent && (
|
||||
<AgentTestChatModal
|
||||
open={isTestModalOpen}
|
||||
onOpenChange={setIsTestModalOpen}
|
||||
agent={node.data.agent}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="p-4 border-b border-neutral-700 flex-shrink-0">
|
||||
<div className="mb-3">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-neutral-500" />
|
||||
<Input
|
||||
placeholder="Search agents..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 bg-neutral-800 border-neutral-700 text-neutral-200 focus-visible:ring-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1">
|
||||
<Select
|
||||
value={selectedFolderId ? selectedFolderId : "all"}
|
||||
onValueChange={handleFolderChange}
|
||||
>
|
||||
<SelectTrigger className="w-full h-9 bg-neutral-800 border-neutral-700 text-neutral-200 focus:ring-emerald-500 focus:ring-offset-0">
|
||||
<SelectValue placeholder="All folders" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||
<SelectItem value="all">All folders</SelectItem>
|
||||
{folders.map((folder) => (
|
||||
<SelectItem key={folder.id} value={folder.id}>
|
||||
{folder.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<Select
|
||||
value={selectedAgentType ? selectedAgentType : "all"}
|
||||
onValueChange={handleAgentTypeChange}
|
||||
>
|
||||
<SelectTrigger className="w-full h-9 bg-neutral-800 border-neutral-700 text-neutral-200 focus:ring-emerald-500 focus:ring-offset-0">
|
||||
<SelectValue placeholder="All types" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||
<SelectItem value="all">All types</SelectItem>
|
||||
{agentTypes.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{getAgentTypeName(type)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col flex-1 min-h-0">
|
||||
<div className="px-4 pt-4 pb-2 flex items-center justify-between flex-shrink-0">
|
||||
<h3 className="text-md font-medium text-neutral-200">
|
||||
{searchQuery ? "Search Results" : "Select an Agent"}
|
||||
</h3>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 bg-emerald-800 hover:bg-emerald-700 border-emerald-700 text-emerald-100"
|
||||
onClick={() => {
|
||||
setNewAgent({
|
||||
id: "",
|
||||
name: "",
|
||||
client_id: clientId || "",
|
||||
type: "llm",
|
||||
model: "",
|
||||
config: {},
|
||||
description: "",
|
||||
});
|
||||
setIsEditMode(false);
|
||||
setIsAgentDialogOpen(true);
|
||||
}}
|
||||
aria-label="New Agent"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-4 scrollbar-hide">
|
||||
<div className="space-y-2 pr-2">
|
||||
{agents.length > 0 ? (
|
||||
agents.map((agent) => (
|
||||
<div
|
||||
key={agent.id}
|
||||
className={`p-3 rounded-md cursor-pointer transition-colors group relative ${
|
||||
node.data.agent?.id === agent.id
|
||||
? "bg-emerald-800/20 border border-emerald-600/40"
|
||||
: "bg-neutral-800 hover:bg-neutral-700 border border-transparent"
|
||||
}`}
|
||||
onClick={() => handleSelectAgent(agent)}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="bg-neutral-700 rounded-full p-1.5 flex-shrink-0">
|
||||
<User size={18} className="text-neutral-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium text-neutral-200 truncate">{agent.name}</h3>
|
||||
<div
|
||||
className="ml-auto text-neutral-400 opacity-0 group-hover:opacity-100 hover:text-yellow-500 transition-colors p-1 rounded hover:bg-yellow-900/20"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setNewAgent({
|
||||
...agent,
|
||||
client_id: clientId || "",
|
||||
});
|
||||
setIsEditMode(true);
|
||||
setIsAgentDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
|
||||
<path d="m15 5 4 4" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs bg-neutral-700 text-emerald-400 border-neutral-600"
|
||||
>
|
||||
{getAgentTypeName(agent.type)}
|
||||
</Badge>
|
||||
{agent.model && (
|
||||
<span className="text-xs text-neutral-400">{agent.model}</span>
|
||||
)}
|
||||
</div>
|
||||
{agent.description && (
|
||||
<p className="text-sm text-neutral-400 mt-1.5 line-clamp-2">
|
||||
{agent.description.slice(0, 30)} {agent.description.length > 30 ? "..." : ""}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-4 text-neutral-400">
|
||||
No agents found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{node.data.agent && (
|
||||
<div className="p-4 border-t border-neutral-700 flex-shrink-0">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-md font-medium text-neutral-200">Selected Agent</h3>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 bg-neutral-700 hover:bg-neutral-600 border-neutral-600 text-neutral-200"
|
||||
onClick={() => {
|
||||
handleUpdateNode({
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
agent: null,
|
||||
},
|
||||
});
|
||||
}}
|
||||
aria-label="Clear agent"
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 bg-neutral-700 hover:bg-neutral-600 border-neutral-600 text-neutral-200"
|
||||
onClick={handleEditAgent}
|
||||
aria-label="Edit agent"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
|
||||
<path d="m15 5 4 4" />
|
||||
</svg>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 bg-emerald-800 hover:bg-emerald-700 border-emerald-700 text-emerald-100"
|
||||
onClick={() => setIsTestModalOpen(true)}
|
||||
aria-label="Test agent"
|
||||
>
|
||||
<PlayIcon size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-md bg-emerald-800/20 border border-emerald-600/40">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="bg-emerald-900/50 rounded-full p-1.5 flex-shrink-0">
|
||||
<User size={18} className="text-emerald-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium text-neutral-200 truncate">{node.data.agent.name}</h3>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs bg-emerald-900/50 text-emerald-400 border-emerald-700/50"
|
||||
>
|
||||
{getAgentTypeName(node.data.agent.type)}
|
||||
</Badge>
|
||||
{node.data.agent.model && (
|
||||
<span className="text-xs text-neutral-400">{node.data.agent.model}</span>
|
||||
)}
|
||||
</div>
|
||||
{node.data.agent.description && (
|
||||
<p className="text-sm text-neutral-400 mt-1.5 line-clamp-2">
|
||||
{node.data.agent.description.slice(0, 30)} {node.data.agent.description.length > 30 ? "..." : ""}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/agent/AgentNode.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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Handle, NodeProps, Position, useEdges } from "@xyflow/react";
|
||||
import { MessageCircle, User, Code, ExternalLink, Workflow, GitBranch, RefreshCw, BookOpenCheck, ArrowRight } from "lucide-react";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { BaseNode } from "../../BaseNode";
|
||||
|
||||
export function AgentNode(props: NodeProps) {
|
||||
const { selected, data } = props;
|
||||
|
||||
const edges = useEdges();
|
||||
|
||||
const isHandleConnected = (handleId: string) => {
|
||||
return edges.some(
|
||||
(edge) => edge.source === props.id && edge.sourceHandle === handleId
|
||||
);
|
||||
};
|
||||
|
||||
const isBottomHandleConnected = isHandleConnected("bottom-handle");
|
||||
|
||||
const agent = data.agent as Agent | undefined;
|
||||
const isExecuting = data.isExecuting as boolean | undefined;
|
||||
|
||||
const getAgentTypeName = (type: string) => {
|
||||
const agentTypes: Record<string, string> = {
|
||||
llm: "LLM Agent",
|
||||
a2a: "A2A Agent",
|
||||
sequential: "Sequential Agent",
|
||||
parallel: "Parallel Agent",
|
||||
loop: "Loop Agent",
|
||||
workflow: "Workflow Agent",
|
||||
task: "Task Agent",
|
||||
};
|
||||
return agentTypes[type] || type;
|
||||
};
|
||||
|
||||
const getAgentTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "llm":
|
||||
return <Code className="h-4 w-4 text-green-400" />;
|
||||
case "a2a":
|
||||
return <ExternalLink className="h-4 w-4 text-indigo-400" />;
|
||||
case "sequential":
|
||||
return <Workflow className="h-4 w-4 text-yellow-400" />;
|
||||
case "parallel":
|
||||
return <GitBranch className="h-4 w-4 text-purple-400" />;
|
||||
case "loop":
|
||||
return <RefreshCw className="h-4 w-4 text-orange-400" />;
|
||||
case "workflow":
|
||||
return <Workflow className="h-4 w-4 text-blue-400" />;
|
||||
case "task":
|
||||
return <BookOpenCheck className="h-4 w-4 text-red-400" />;
|
||||
default:
|
||||
return <User className="h-4 w-4 text-neutral-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getModelBadgeColor = (model: string) => {
|
||||
if (model?.includes('gpt-4')) return 'bg-green-900/30 text-green-400 border-green-600/30';
|
||||
if (model?.includes('gpt-3')) return 'bg-yellow-900/30 text-yellow-400 border-yellow-600/30';
|
||||
if (model?.includes('claude')) return 'bg-orange-900/30 text-orange-400 border-orange-600/30';
|
||||
if (model?.includes('gemini')) return 'bg-blue-900/30 text-blue-400 border-blue-600/30';
|
||||
return 'bg-neutral-800 text-neutral-400 border-neutral-600/50';
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseNode hasTarget={true} selected={selected || false} borderColor="blue" isExecuting={isExecuting}>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-900/40 shadow-sm">
|
||||
<User className="h-5 w-5 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-medium text-blue-400">
|
||||
{data.label as string}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{agent ? (
|
||||
<div className="mb-3 rounded-lg border border-blue-700/40 bg-blue-950/10 p-3 transition-all duration-200 hover:border-blue-600/50 hover:bg-blue-900/10">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center">
|
||||
{getAgentTypeIcon(agent.type)}
|
||||
<span className="ml-1.5 font-medium text-white">{agent.name}</span>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="px-1.5 py-0 text-xs bg-blue-900/30 text-blue-400 border-blue-700/40"
|
||||
>
|
||||
{getAgentTypeName(agent.type)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{agent.model && (
|
||||
<div className="mt-2 flex items-center">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn("px-1.5 py-0 text-xs", getModelBadgeColor(agent.model))}
|
||||
>
|
||||
{agent.model}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{agent.description && (
|
||||
<p className="mt-2 text-xs text-neutral-400 line-clamp-2">
|
||||
{agent.description.slice(0, 30)} {agent.description.length > 30 && '...'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-3 flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-blue-700/40 bg-blue-950/10 p-5 text-center transition-all duration-200 hover:border-blue-600/50 hover:bg-blue-900/20">
|
||||
<User className="h-8 w-8 text-blue-700/50 mb-2" />
|
||||
<p className="text-blue-400">Select an agent</p>
|
||||
<p className="mt-1 text-xs text-neutral-500">Click to configure</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 flex items-center justify-end text-sm text-neutral-400 transition-colors">
|
||||
<div className="flex items-center space-x-1 rounded-md py-1 px-2">
|
||||
<span>Next step</span>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<Handle
|
||||
className={cn(
|
||||
"!w-3 !h-3 !rounded-full transition-all duration-300",
|
||||
isBottomHandleConnected ? "!bg-blue-500 !border-blue-400" : "!bg-neutral-400 !border-neutral-500",
|
||||
selected && isBottomHandleConnected && "!bg-blue-400 !border-blue-300"
|
||||
)}
|
||||
style={{
|
||||
right: "-8px",
|
||||
top: "calc(100% - 25px)",
|
||||
}}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="bottom-handle"
|
||||
/>
|
||||
</div>
|
||||
</BaseNode>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/agent/AgentTestChatModal.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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useAgentWebSocket } from "@/hooks/use-agent-webSocket";
|
||||
import { getAccessTokenFromCookie, cn } from "@/lib/utils";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { ChatInput } from "@/app/chat/components/ChatInput";
|
||||
import { ChatMessage as ChatMessageComponent } from "@/app/chat/components/ChatMessage";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { ChatPart } from "@/services/sessionService";
|
||||
import { FileData } from "@/lib/file-utils";
|
||||
import { X, User, Bot, Zap, MessageSquare, Loader2, Code, ExternalLink, Workflow, RefreshCw } from "lucide-react";
|
||||
|
||||
interface FunctionMessageContent {
|
||||
title: string;
|
||||
content: string;
|
||||
author?: string;
|
||||
}
|
||||
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
content: any;
|
||||
author: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface AgentTestChatModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
agent: Agent;
|
||||
canvasRef?: React.RefObject<any>;
|
||||
}
|
||||
|
||||
export function AgentTestChatModal({ open, onOpenChange, agent, canvasRef }: AgentTestChatModalProps) {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const [expandedFunctions, setExpandedFunctions] = useState<Record<string, boolean>>({});
|
||||
const [isInitializing, setIsInitializing] = useState(true);
|
||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const user = typeof window !== "undefined" ? JSON.parse(localStorage.getItem("user") || "{}") : {};
|
||||
const clientId = user?.client_id || "test";
|
||||
|
||||
const generateExternalId = () => {
|
||||
const now = new Date();
|
||||
return (
|
||||
now.getFullYear().toString() +
|
||||
(now.getMonth() + 1).toString().padStart(2, "0") +
|
||||
now.getDate().toString().padStart(2, "0") +
|
||||
now.getHours().toString().padStart(2, "0") +
|
||||
now.getMinutes().toString().padStart(2, "0") +
|
||||
now.getSeconds().toString().padStart(2, "0") +
|
||||
now.getMilliseconds().toString().padStart(3, "0")
|
||||
);
|
||||
};
|
||||
|
||||
const [externalId, setExternalId] = useState(generateExternalId());
|
||||
const jwt = getAccessTokenFromCookie();
|
||||
|
||||
const onEvent = useCallback((event: any) => {
|
||||
setMessages((prev) => [...prev, event]);
|
||||
|
||||
// Check if the message comes from a workflow node and highlight the node
|
||||
// only if the canvasRef is available (called from Test Workflow on the main page)
|
||||
if (event.author && event.author.startsWith('workflow-node:') && canvasRef?.current) {
|
||||
const nodeId = event.author.split(':')[1];
|
||||
canvasRef.current.setActiveExecutionNodeId(nodeId);
|
||||
}
|
||||
}, [canvasRef]);
|
||||
|
||||
const onTurnComplete = useCallback(() => {
|
||||
setIsSending(false);
|
||||
}, []);
|
||||
|
||||
const { sendMessage: wsSendMessage, disconnect } = useAgentWebSocket({
|
||||
agentId: agent.id,
|
||||
externalId,
|
||||
jwt,
|
||||
onEvent,
|
||||
onTurnComplete,
|
||||
});
|
||||
|
||||
// Handle ESC key to close the panel
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && open) {
|
||||
onOpenChange(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [onOpenChange, open]);
|
||||
|
||||
// Show initialization state for better UX
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setIsInitializing(true);
|
||||
const timer = setTimeout(() => {
|
||||
setIsInitializing(false);
|
||||
}, 1200);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [open, externalId]);
|
||||
|
||||
const handleRestartChat = () => {
|
||||
if (disconnect) disconnect();
|
||||
setMessages([]);
|
||||
setExpandedFunctions({});
|
||||
setExternalId(generateExternalId());
|
||||
setIsInitializing(true);
|
||||
|
||||
// Short delay to show the initialization status
|
||||
const timer = setTimeout(() => {
|
||||
setIsInitializing(false);
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
const handleSendMessageWithFiles = (message: string, files?: FileData[]) => {
|
||||
if ((!message.trim() && (!files || files.length === 0))) return;
|
||||
setIsSending(true);
|
||||
|
||||
const messageParts: ChatPart[] = [];
|
||||
|
||||
if (message.trim()) {
|
||||
messageParts.push({ text: message });
|
||||
}
|
||||
|
||||
if (files && files.length > 0) {
|
||||
files.forEach(file => {
|
||||
messageParts.push({
|
||||
inline_data: {
|
||||
data: file.data,
|
||||
mime_type: file.content_type,
|
||||
metadata: {
|
||||
filename: file.filename
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: `temp-${Date.now()}`,
|
||||
content: {
|
||||
parts: messageParts,
|
||||
role: "user"
|
||||
},
|
||||
author: "user",
|
||||
timestamp: Date.now() / 1000,
|
||||
},
|
||||
]);
|
||||
|
||||
wsSendMessage(message, files);
|
||||
};
|
||||
|
||||
const containsMarkdown = (text: string): boolean => {
|
||||
if (!text || text.length < 3) return false;
|
||||
const markdownPatterns = [
|
||||
/[*_]{1,2}[^*_]+[*_]{1,2}/, // bold/italic
|
||||
/\[[^\]]+\]\([^)]+\)/, // links
|
||||
/^#{1,6}\s/m, // headers
|
||||
/^[-*+]\s/m, // unordered lists
|
||||
/^[0-9]+\.\s/m, // ordered lists
|
||||
/^>\s/m, // block quotes
|
||||
/`[^`]+`/, // inline code
|
||||
/```[\s\S]*?```/, // code blocks
|
||||
/^\|(.+\|)+$/m, // tables
|
||||
/!\[[^\]]*\]\([^)]+\)/, // images
|
||||
];
|
||||
return markdownPatterns.some((pattern) => pattern.test(text));
|
||||
};
|
||||
|
||||
const getMessageText = (message: ChatMessage): string | FunctionMessageContent => {
|
||||
const author = message.author;
|
||||
const parts = message.content.parts;
|
||||
if (!parts || parts.length === 0) return "Empty content";
|
||||
const functionCallPart = parts.find((part: any) => part.functionCall || part.function_call);
|
||||
const functionResponsePart = parts.find((part: any) => part.functionResponse || part.function_response);
|
||||
|
||||
const inlineDataParts = parts.filter((part: any) => part.inline_data);
|
||||
|
||||
if (functionCallPart) {
|
||||
const funcCall = functionCallPart.functionCall || functionCallPart.function_call || {};
|
||||
const args = funcCall.args || {};
|
||||
const name = funcCall.name || "unknown";
|
||||
const id = funcCall.id || "no-id";
|
||||
return {
|
||||
author,
|
||||
title: `📞 Function call: ${name}`,
|
||||
content: `ID: ${id}\nArgs: ${Object.keys(args).length > 0 ? `\n${JSON.stringify(args, null, 2)}` : "{}"}`,
|
||||
} as FunctionMessageContent;
|
||||
}
|
||||
if (functionResponsePart) {
|
||||
const funcResponse = functionResponsePart.functionResponse || functionResponsePart.function_response || {};
|
||||
const response = funcResponse.response || {};
|
||||
const name = funcResponse.name || "unknown";
|
||||
const id = funcResponse.id || "no-id";
|
||||
const status = response.status || "unknown";
|
||||
const statusEmoji = status === "error" ? "❌" : "✅";
|
||||
let resultText = "";
|
||||
if (status === "error") {
|
||||
resultText = `Error: ${response.error_message || "Unknown error"}`;
|
||||
} else if (response.report) {
|
||||
resultText = `Result: ${response.report}`;
|
||||
} else if (response.result && response.result.content) {
|
||||
const content = response.result.content;
|
||||
if (Array.isArray(content) && content.length > 0 && content[0].text) {
|
||||
try {
|
||||
const textContent = content[0].text;
|
||||
const parsedJson = JSON.parse(textContent);
|
||||
resultText = `Result: \n${JSON.stringify(parsedJson, null, 2)}`;
|
||||
} catch (e) {
|
||||
resultText = `Result: ${content[0].text}`;
|
||||
}
|
||||
} else {
|
||||
resultText = `Result:\n${JSON.stringify(response, null, 2)}`;
|
||||
}
|
||||
} else {
|
||||
resultText = `Result:\n${JSON.stringify(response, null, 2)}`;
|
||||
}
|
||||
return {
|
||||
author,
|
||||
title: `${statusEmoji} Function response: ${name}`,
|
||||
content: `ID: ${id}\n${resultText}`,
|
||||
} as FunctionMessageContent;
|
||||
}
|
||||
|
||||
if (parts.length === 1 && parts[0].text) {
|
||||
return {
|
||||
author,
|
||||
content: parts[0].text,
|
||||
title: "Message",
|
||||
} as FunctionMessageContent;
|
||||
}
|
||||
const textParts = parts.filter((part: any) => part.text).map((part: any) => part.text).filter((text: string) => text);
|
||||
if (textParts.length > 0) {
|
||||
return {
|
||||
author,
|
||||
content: textParts.join("\n\n"),
|
||||
title: "Message",
|
||||
} as FunctionMessageContent;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(parts, null, 2).replace(/\\n/g, "\n");
|
||||
} catch (error) {
|
||||
return "Unable to interpret message content";
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFunctionExpansion = (messageId: string) => {
|
||||
setExpandedFunctions((prev) => ({ ...prev, [messageId]: !prev[messageId] }));
|
||||
};
|
||||
|
||||
const getAgentTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "llm":
|
||||
return <Code className="h-4 w-4 text-green-400" />;
|
||||
case "a2a":
|
||||
return <ExternalLink className="h-4 w-4 text-indigo-400" />;
|
||||
case "sequential":
|
||||
case "workflow":
|
||||
return <Workflow className="h-4 w-4 text-blue-400" />;
|
||||
default:
|
||||
return <Bot className="h-4 w-4 text-emerald-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
// Scroll to bottom whenever messages change
|
||||
useEffect(() => {
|
||||
if (messagesContainerRef.current) {
|
||||
messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
// Use React Portal to render directly to document body, bypassing all parent containers
|
||||
const modalContent = (
|
||||
<>
|
||||
{/* Overlay for mobile */}
|
||||
<div
|
||||
className="md:hidden fixed inset-0 bg-black bg-opacity-50 z-[15] transition-opacity duration-300"
|
||||
onClick={() => onOpenChange(false)}
|
||||
/>
|
||||
|
||||
{/* Side panel */}
|
||||
<div
|
||||
className="fixed right-0 top-0 z-[1000] h-full w-[450px] bg-gradient-to-b from-neutral-900 to-neutral-950 border-l border-neutral-800 shadow-2xl flex flex-col transition-all duration-300 ease-in-out transform"
|
||||
style={{
|
||||
transform: open ? 'translateX(0)' : 'translateX(100%)',
|
||||
boxShadow: '0 0 25px rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex-shrink-0 p-5 bg-gradient-to-r from-neutral-900 to-neutral-800 border-b border-neutral-800">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center mb-1">
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-emerald-600 to-emerald-900 flex items-center justify-center shadow-lg mr-3">
|
||||
{getAgentTypeIcon(agent.type)}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">{agent.name}</h2>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge
|
||||
className="bg-emerald-900/40 text-emerald-400 border border-emerald-700/50 px-2"
|
||||
>
|
||||
{agent.type.toUpperCase()} Agent
|
||||
</Badge>
|
||||
{agent.model && (
|
||||
<span className="text-xs text-neutral-400 bg-neutral-800/60 px-2 py-0.5 rounded-md">
|
||||
{agent.model}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={handleRestartChat}
|
||||
className="p-1.5 rounded-full hover:bg-neutral-700/50 text-neutral-400 hover:text-white transition-colors group relative"
|
||||
title="Restart chat"
|
||||
disabled={isInitializing}
|
||||
>
|
||||
<RefreshCw size={18} className={isInitializing ? "animate-spin text-emerald-400" : ""} />
|
||||
<span className="absolute -bottom-8 right-0 bg-neutral-800 text-neutral-200 text-xs rounded py-1 px-2 opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap">
|
||||
Restart chat
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="p-1.5 rounded-full hover:bg-neutral-700/50 text-neutral-400 hover:text-white transition-colors group relative"
|
||||
>
|
||||
<X size={18} />
|
||||
<span className="absolute -bottom-8 right-0 bg-neutral-800 text-neutral-200 text-xs rounded py-1 px-2 opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap">
|
||||
Close
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{agent.description && (
|
||||
<div className="mt-3 text-sm text-neutral-400 bg-neutral-800/30 p-3 rounded-md border border-neutral-800">
|
||||
{agent.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Chat content */}
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden p-3 bg-gradient-to-b from-neutral-900/50 to-neutral-950" ref={messagesContainerRef}>
|
||||
{isInitializing ? (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-emerald-500 to-emerald-700 flex items-center justify-center shadow-lg mb-4 animate-pulse">
|
||||
<Zap className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<p className="text-neutral-400 mb-2">Initializing connection...</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-bounce"
|
||||
style={{ animationDelay: '0ms' }}></span>
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-bounce"
|
||||
style={{ animationDelay: '150ms' }}></span>
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-bounce"
|
||||
style={{ animationDelay: '300ms' }}></span>
|
||||
</div>
|
||||
</div>
|
||||
) : messages.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center px-6">
|
||||
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-blue-500/20 to-emerald-500/20 flex items-center justify-center shadow-lg mb-5 border border-emerald-500/30">
|
||||
<MessageSquare className="h-6 w-6 text-emerald-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-neutral-300 mb-2">Start the conversation</h3>
|
||||
<p className="text-neutral-500 text-sm max-w-xs">
|
||||
Type a message below to begin chatting with {agent.name}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 w-full max-w-full">
|
||||
{messages.map((message) => {
|
||||
const messageContent = getMessageText(message);
|
||||
const agentColor = message.author === "user" ? "bg-emerald-500" : "bg-gradient-to-br from-neutral-800 to-neutral-900";
|
||||
const isExpanded = expandedFunctions[message.id] || false;
|
||||
|
||||
return (
|
||||
<ChatMessageComponent
|
||||
key={message.id}
|
||||
message={message}
|
||||
agentColor={agentColor}
|
||||
isExpanded={isExpanded}
|
||||
toggleExpansion={toggleFunctionExpansion}
|
||||
containsMarkdown={containsMarkdown}
|
||||
messageContent={messageContent}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{isSending && (
|
||||
<div className="flex justify-start">
|
||||
<div className="flex gap-3 max-w-[80%]">
|
||||
<Avatar
|
||||
className="bg-gradient-to-br from-purple-600 to-purple-800 shadow-md border-0"
|
||||
>
|
||||
<AvatarFallback className="bg-transparent">
|
||||
<Bot className="h-4 w-4 text-white" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="rounded-lg p-3 bg-gradient-to-br from-neutral-800 to-neutral-900 border border-neutral-700/50">
|
||||
<div className="flex space-x-2">
|
||||
<div className="h-2 w-2 rounded-full bg-emerald-400 animate-bounce"></div>
|
||||
<div className="h-2 w-2 rounded-full bg-emerald-400 animate-bounce [animation-delay:0.2s]"></div>
|
||||
<div className="h-2 w-2 rounded-full bg-emerald-400 animate-bounce [animation-delay:0.4s]"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Message input */}
|
||||
<div className="p-3 border-t border-neutral-800 bg-neutral-900">
|
||||
<ChatInput
|
||||
onSendMessage={handleSendMessageWithFiles}
|
||||
isLoading={isSending}
|
||||
placeholder="Type your message..."
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
// Use createPortal to render the modal directly to the document body
|
||||
return typeof document !== 'undefined'
|
||||
? createPortal(modalContent, document.body)
|
||||
: null;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/agent/styles.css │
|
||||
│ 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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
|
||||
.markdown-content {
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.markdown-content pre {
|
||||
white-space: pre-wrap;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.markdown-content code {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.markdown-content p {
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.markdown-content table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.markdown-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.overflow-wrap-anywhere {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/condition/ConditionDialog.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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useState } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
import { ConditionType, ConditionTypeEnum } from "../../nodeFunctions";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Filter, ArrowRight } from "lucide-react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@/components/ui/select";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
const conditionTypes = [
|
||||
{
|
||||
id: "previous-output",
|
||||
name: "Previous output",
|
||||
description: "Validate the result returned by the previous node",
|
||||
icon: <Filter className="h-5 w-5 text-blue-400" />,
|
||||
color: "bg-blue-900/30 border-blue-700/50",
|
||||
},
|
||||
];
|
||||
|
||||
const operators = [
|
||||
{ value: "is_defined", label: "is defined" },
|
||||
{ value: "is_not_defined", label: "is not defined" },
|
||||
{ value: "equals", label: "is equal to" },
|
||||
{ value: "not_equals", label: "is not equal to" },
|
||||
{ value: "contains", label: "contains" },
|
||||
{ value: "not_contains", label: "does not contain" },
|
||||
{ value: "starts_with", label: "starts with" },
|
||||
{ value: "ends_with", label: "ends with" },
|
||||
{ value: "greater_than", label: "is greater than" },
|
||||
{ value: "greater_than_or_equal", label: "is greater than or equal to" },
|
||||
{ value: "less_than", label: "is less than" },
|
||||
{ value: "less_than_or_equal", label: "is less than or equal to" },
|
||||
{ value: "matches", label: "matches the regex" },
|
||||
{ value: "not_matches", label: "does not match the regex" },
|
||||
];
|
||||
|
||||
const outputFields = [
|
||||
{ value: "content", label: "Content" },
|
||||
{ value: "status", label: "Status" },
|
||||
];
|
||||
|
||||
function ConditionDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
selectedNode,
|
||||
handleUpdateNode,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
selectedNode: any;
|
||||
handleUpdateNode: any;
|
||||
}) {
|
||||
const [selectedType, setSelectedType] = useState("previous-output");
|
||||
const [selectedField, setSelectedField] = useState(outputFields[0].value);
|
||||
const [selectedOperator, setSelectedOperator] = useState(operators[0].value);
|
||||
const [comparisonValue, setComparisonValue] = useState("");
|
||||
|
||||
const handleConditionSave = (condition: ConditionType) => {
|
||||
const newConditions = selectedNode.data.conditions
|
||||
? [...selectedNode.data.conditions]
|
||||
: [];
|
||||
newConditions.push(condition);
|
||||
|
||||
const updatedNode = {
|
||||
...selectedNode,
|
||||
data: {
|
||||
...selectedNode.data,
|
||||
conditions: newConditions,
|
||||
},
|
||||
};
|
||||
|
||||
handleUpdateNode(updatedNode);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const getOperatorLabel = (value: string) => {
|
||||
return operators.find(op => op.value === value)?.label || value;
|
||||
};
|
||||
|
||||
const getFieldLabel = (value: string) => {
|
||||
return outputFields.find(field => field.value === value)?.label || value;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="bg-neutral-800 border-neutral-700 text-neutral-200 sm:max-w-[650px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New Condition</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-6 py-4">
|
||||
<div className="grid gap-4">
|
||||
<Label className="text-sm font-medium">Condition Type</Label>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{conditionTypes.map((type) => (
|
||||
<div
|
||||
key={type.id}
|
||||
className={`flex items-center space-x-3 rounded-md border p-3 cursor-pointer transition-all ${
|
||||
selectedType === type.id
|
||||
? "bg-blue-900/30 border-blue-600"
|
||||
: "border-neutral-700 hover:border-blue-700/50 hover:bg-neutral-700/50"
|
||||
}`}
|
||||
onClick={() => setSelectedType(type.id)}
|
||||
>
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-blue-900/40">
|
||||
{type.icon}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium">{type.name}</h4>
|
||||
<p className="text-xs text-neutral-400">{type.description}</p>
|
||||
</div>
|
||||
{selectedType === type.id && (
|
||||
<Badge className="bg-blue-600 text-neutral-100">Selected</Badge>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">Configuration</Label>
|
||||
{selectedType === "previous-output" && (
|
||||
<div className="flex items-center gap-2 text-sm text-neutral-400">
|
||||
<span>Output field</span>
|
||||
<ArrowRight className="h-3 w-3" />
|
||||
<span>Operator</span>
|
||||
{!["is_defined", "is_not_defined"].includes(selectedOperator) && (
|
||||
<>
|
||||
<ArrowRight className="h-3 w-3" />
|
||||
<span>Value</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedType === "previous-output" && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="field">Output Field</Label>
|
||||
<Select
|
||||
value={selectedField}
|
||||
onValueChange={setSelectedField}
|
||||
>
|
||||
<SelectTrigger id="field" className="bg-neutral-700 border-neutral-600">
|
||||
<SelectValue placeholder="Select field" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-neutral-700 border-neutral-600">
|
||||
{outputFields.map((field) => (
|
||||
<SelectItem key={field.value} value={field.value}>
|
||||
{field.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="operator">Operator</Label>
|
||||
<Select
|
||||
value={selectedOperator}
|
||||
onValueChange={setSelectedOperator}
|
||||
>
|
||||
<SelectTrigger id="operator" className="bg-neutral-700 border-neutral-600">
|
||||
<SelectValue placeholder="Select operator" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-neutral-700 border-neutral-600">
|
||||
{operators.map((op) => (
|
||||
<SelectItem key={op.value} value={op.value}>
|
||||
{op.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{!["is_defined", "is_not_defined"].includes(selectedOperator) && (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="value">Comparison Value</Label>
|
||||
<Input
|
||||
id="value"
|
||||
value={comparisonValue}
|
||||
onChange={(e) => setComparisonValue(e.target.value)}
|
||||
className="bg-neutral-700 border-neutral-600"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-md bg-neutral-700/50 border border-neutral-600 p-3 mt-4">
|
||||
<div className="text-sm font-medium text-neutral-400 mb-1">Preview</div>
|
||||
<div className="text-sm">
|
||||
<span className="text-blue-400 font-medium">{getFieldLabel(selectedField)}</span>
|
||||
{" "}
|
||||
<span className="text-neutral-300">{getOperatorLabel(selectedOperator)}</span>
|
||||
{" "}
|
||||
{!["is_defined", "is_not_defined"].includes(selectedOperator) && (
|
||||
<span className="text-emerald-400 font-medium">"{comparisonValue || "(empty)"}"</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="border-neutral-600 text-neutral-200 hover:bg-neutral-700"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleConditionSave({
|
||||
id: uuidv4(),
|
||||
type: ConditionTypeEnum.PREVIOUS_OUTPUT,
|
||||
data: {
|
||||
field: selectedField,
|
||||
operator: selectedOperator,
|
||||
value: comparisonValue,
|
||||
},
|
||||
});
|
||||
}}
|
||||
className="bg-blue-700 hover:bg-blue-600 text-white"
|
||||
>
|
||||
Add Condition
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { ConditionDialog };
|
||||
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/condition/ConditionForm.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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
import { ConditionType, ConditionTypeEnum } from "../../nodeFunctions";
|
||||
import { ConditionDialog } from "./ConditionDialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Filter, Trash2, Plus } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
function ConditionForm({
|
||||
selectedNode,
|
||||
handleUpdateNode,
|
||||
}: {
|
||||
selectedNode: any;
|
||||
handleUpdateNode: any;
|
||||
setEdges: any;
|
||||
setIsOpen: any;
|
||||
setSelectedNode: any;
|
||||
}) {
|
||||
const [node, setNode] = useState(selectedNode);
|
||||
|
||||
const [conditions, setConditions] = useState<ConditionType[]>(
|
||||
selectedNode.data.conditions || []
|
||||
);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [deleteDialog, setDeleteDialog] = useState(false);
|
||||
const [conditionToDelete, setConditionToDelete] =
|
||||
useState<ConditionType | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedNode) {
|
||||
setNode(selectedNode);
|
||||
setConditions(selectedNode.data.conditions || []);
|
||||
}
|
||||
}, [selectedNode]);
|
||||
|
||||
const handleDelete = (condition: ConditionType) => {
|
||||
setConditionToDelete(condition);
|
||||
setDeleteDialog(true);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
if (!conditionToDelete) return;
|
||||
|
||||
const newConditions = conditions.filter(
|
||||
(c) => c.id !== conditionToDelete.id
|
||||
);
|
||||
setConditions(newConditions);
|
||||
handleUpdateNode({
|
||||
...node,
|
||||
data: { ...node.data, conditions: newConditions },
|
||||
});
|
||||
setDeleteDialog(false);
|
||||
setConditionToDelete(null);
|
||||
};
|
||||
|
||||
const renderCondition = (condition: ConditionType) => {
|
||||
if (condition.type === ConditionTypeEnum.PREVIOUS_OUTPUT) {
|
||||
type OperatorType =
|
||||
| "is_defined"
|
||||
| "is_not_defined"
|
||||
| "equals"
|
||||
| "not_equals"
|
||||
| "contains"
|
||||
| "not_contains"
|
||||
| "starts_with"
|
||||
| "ends_with"
|
||||
| "greater_than"
|
||||
| "greater_than_or_equal"
|
||||
| "less_than"
|
||||
| "less_than_or_equal"
|
||||
| "matches"
|
||||
| "not_matches";
|
||||
|
||||
const operatorText: Record<OperatorType, string> = {
|
||||
is_defined: "is defined",
|
||||
is_not_defined: "is not defined",
|
||||
equals: "is equal to",
|
||||
not_equals: "is not equal to",
|
||||
contains: "contains",
|
||||
not_contains: "does not contain",
|
||||
starts_with: "starts with",
|
||||
ends_with: "ends with",
|
||||
greater_than: "is greater than",
|
||||
greater_than_or_equal: "is greater than or equal to",
|
||||
less_than: "is less than",
|
||||
less_than_or_equal: "is less than or equal to",
|
||||
matches: "matches the regex",
|
||||
not_matches: "does not match the regex",
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={condition.id}
|
||||
className="p-3 rounded-md cursor-pointer transition-colors bg-neutral-800 hover:bg-neutral-700 border border-neutral-700 mb-2 group"
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="bg-blue-900/50 rounded-full p-1.5 flex-shrink-0">
|
||||
<Filter size={18} className="text-blue-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium text-neutral-200">Condition</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDelete(condition)}
|
||||
className="h-7 w-7 text-neutral-400 opacity-0 group-hover:opacity-100 hover:text-red-500 hover:bg-red-900/20"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs bg-blue-900/20 text-blue-400 border-blue-700/50"
|
||||
>
|
||||
Field
|
||||
</Badge>
|
||||
<span className="text-sm text-neutral-300 font-medium">{condition.data.field}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-1 mt-1.5">
|
||||
<span className="text-sm text-neutral-400">{operatorText[condition.data.operator as OperatorType]}</span>
|
||||
{!["is_defined", "is_not_defined"].includes(condition.data.operator) && (
|
||||
<span className="text-sm font-medium text-emerald-400">
|
||||
"{condition.data.value}"
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="p-4 border-b border-neutral-700 flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-md font-medium text-neutral-200">Logic Type</h3>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs bg-blue-900/20 text-blue-400 border-blue-700/50"
|
||||
>
|
||||
{node.data.type === "or" ? "ANY" : "ALL"}
|
||||
</Badge>
|
||||
</div>
|
||||
<Select
|
||||
value={node.data.type || "and"}
|
||||
onValueChange={(value) => {
|
||||
const updatedNode = {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
type: value,
|
||||
},
|
||||
};
|
||||
setNode(updatedNode);
|
||||
handleUpdateNode(updatedNode);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[120px] h-8 bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||
<SelectValue placeholder="Select type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||
<SelectItem value="and">ALL (AND)</SelectItem>
|
||||
<SelectItem value="or">ANY (OR)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<p className="text-sm text-neutral-400 mt-2">
|
||||
{node.data.type === "or"
|
||||
? "Any of the following conditions must be true to proceed."
|
||||
: "All of the following conditions must be true to proceed."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-4 py-4 min-h-0">
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-md font-medium text-neutral-200">Conditions</h3>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 bg-blue-800/20 hover:bg-blue-700/30 border-blue-700/50 text-blue-300"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Plus size={14} className="mr-1" />
|
||||
Add Condition
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{conditions.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{conditions.map((condition) => renderCondition(condition))}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
onClick={() => setOpen(true)}
|
||||
className="flex flex-col items-center justify-center p-6 rounded-lg border-2 border-dashed border-neutral-700 hover:border-blue-600/50 hover:bg-neutral-800/50 transition-colors cursor-pointer text-center"
|
||||
>
|
||||
<Filter className="h-10 w-10 text-neutral-500 mb-2" />
|
||||
<p className="text-neutral-400">No conditions yet</p>
|
||||
<p className="text-sm text-neutral-500 mt-1">Click to add a condition</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConditionDialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
selectedNode={selectedNode}
|
||||
handleUpdateNode={handleUpdateNode}
|
||||
/>
|
||||
|
||||
<Dialog open={deleteDialog} onOpenChange={setDeleteDialog}>
|
||||
<DialogContent className="bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Confirm Delete</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<p>Are you sure you want to delete this condition?</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-neutral-600 text-neutral-300 hover:bg-neutral-700"
|
||||
onClick={() => {
|
||||
setDeleteDialog(false);
|
||||
setConditionToDelete(null);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="bg-red-900 hover:bg-red-800 text-white"
|
||||
onClick={confirmDelete}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ConditionForm };
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/condition/ConditionNode.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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Handle, Node, NodeProps, Position, useEdges } from "@xyflow/react";
|
||||
import { FilterIcon, ArrowRight } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { BaseNode } from "../../BaseNode";
|
||||
import { ConditionType, ConditionTypeEnum } from "../../nodeFunctions";
|
||||
|
||||
export type ConditionNodeType = Node<
|
||||
{
|
||||
label?: string;
|
||||
type?: "and" | "or";
|
||||
conditions?: ConditionType[];
|
||||
},
|
||||
"condition-node"
|
||||
>;
|
||||
|
||||
export type OperatorType =
|
||||
| "is_defined"
|
||||
| "is_not_defined"
|
||||
| "equals"
|
||||
| "not_equals"
|
||||
| "contains"
|
||||
| "not_contains"
|
||||
| "starts_with"
|
||||
| "ends_with"
|
||||
| "greater_than"
|
||||
| "greater_than_or_equal"
|
||||
| "less_than"
|
||||
| "less_than_or_equal"
|
||||
| "matches"
|
||||
| "not_matches";
|
||||
|
||||
const operatorText: Record<OperatorType, string> = {
|
||||
equals: "is equal to",
|
||||
not_equals: "is not equal to",
|
||||
contains: "contains",
|
||||
not_contains: "does not contain",
|
||||
starts_with: "starts with",
|
||||
ends_with: "ends with",
|
||||
greater_than: "is greater than",
|
||||
greater_than_or_equal: "is greater than or equal to",
|
||||
less_than: "is less than",
|
||||
less_than_or_equal: "is less than or equal to",
|
||||
matches: "matches the pattern",
|
||||
not_matches: "does not match the pattern",
|
||||
is_defined: "is defined",
|
||||
is_not_defined: "is not defined",
|
||||
};
|
||||
|
||||
export function ConditionNode(props: NodeProps) {
|
||||
const { selected, data } = props;
|
||||
const edges = useEdges();
|
||||
const isExecuting = data.isExecuting as boolean | undefined;
|
||||
|
||||
const typeText = {
|
||||
and: "all of the following conditions",
|
||||
or: "any of the following conditions",
|
||||
};
|
||||
|
||||
const isHandleConnected = (handleId: string) => {
|
||||
return edges.some(
|
||||
(edge) => edge.source === props.id && edge.sourceHandle === handleId,
|
||||
);
|
||||
};
|
||||
|
||||
const isBottomHandleConnected = isHandleConnected("bottom-handle");
|
||||
|
||||
const conditions: ConditionType[] = data.conditions as ConditionType[];
|
||||
// const statistics: StatisticType = data.statistics as StatisticType;
|
||||
|
||||
const renderCondition = (condition: ConditionType) => {
|
||||
const isConnected = isHandleConnected(condition.id);
|
||||
|
||||
if (condition.type === ConditionTypeEnum.PREVIOUS_OUTPUT) {
|
||||
return (
|
||||
<div
|
||||
className="mb-3 cursor-pointer rounded-lg border border-purple-700/40 bg-purple-950/10 p-3 text-left transition-all duration-200 hover:border-purple-600/50 hover:bg-purple-900/10"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-neutral-300">
|
||||
O campo{" "}
|
||||
<span className="font-semibold text-purple-400">
|
||||
{condition.data.field}
|
||||
</span>{" "}
|
||||
<span className="text-neutral-400">
|
||||
{operatorText[condition.data.operator as OperatorType]}
|
||||
</span>{" "}
|
||||
{!["is_defined", "is_not_defined"].includes(
|
||||
condition.data.operator,
|
||||
) && (
|
||||
<span className="font-semibold text-green-400">
|
||||
"{condition.data.value}"
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Handle
|
||||
className={cn(
|
||||
"!rounded-full transition-all duration-300",
|
||||
isConnected ? "!bg-purple-500 !border-purple-400" : "!bg-neutral-400 !border-neutral-500"
|
||||
)}
|
||||
style={{
|
||||
top: "50%",
|
||||
right: "-5px",
|
||||
transform: "translateY(-50%)",
|
||||
height: "14px",
|
||||
position: "relative",
|
||||
width: "14px",
|
||||
}}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id={condition.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseNode hasTarget={true} selected={selected || false} borderColor="purple" isExecuting={isExecuting}>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-purple-900/40 shadow-sm">
|
||||
<FilterIcon className="h-5 w-5 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-medium text-purple-400">
|
||||
{data.label as string}
|
||||
</p>
|
||||
<p className="text-xs text-neutral-400">
|
||||
Matches {typeText[(data.type as "and" | "or") || "and"]}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{conditions && conditions.length > 0 && Array.isArray(conditions) ? (
|
||||
conditions.map((condition) => (
|
||||
<div key={condition.id}>{renderCondition(condition)}</div>
|
||||
))
|
||||
) : (
|
||||
<div className="mb-3 flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-purple-700/40 bg-purple-950/10 p-5 text-center transition-all duration-200 hover:border-purple-600/50 hover:bg-purple-900/20">
|
||||
<FilterIcon className="h-8 w-8 text-purple-700/50 mb-2" />
|
||||
<p className="text-purple-400">No conditions configured</p>
|
||||
<p className="mt-1 text-xs text-neutral-500">Click to add a condition</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 flex items-center justify-end text-sm text-neutral-400 transition-colors">
|
||||
<div className="flex items-center space-x-1 rounded-md py-1 px-2">
|
||||
<span>Next step</span>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<Handle
|
||||
className={cn(
|
||||
"!w-3 !h-3 !rounded-full transition-all duration-300",
|
||||
isBottomHandleConnected ? "!bg-purple-500 !border-purple-400" : "!bg-neutral-400 !border-neutral-500",
|
||||
selected && isBottomHandleConnected && "!bg-purple-400 !border-purple-300"
|
||||
)}
|
||||
style={{
|
||||
right: "0px",
|
||||
top: "calc(100% - 25px)",
|
||||
}}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="bottom-handle"
|
||||
/>
|
||||
</div>
|
||||
</BaseNode>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @author: Victor Calazans - Implementation of Delay node form │
|
||||
│ @file: /app/agents/workflows/nodes/components/delay/DelayForm.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Delay form developed by: Victor Calazans │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Delay form implementation date: May 17, 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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useState, useEffect } from "react";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { Clock, Trash2, Save, AlertCircle, HourglassIcon } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
import { DelayType, DelayUnitEnum } from "../../nodeFunctions";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function DelayForm({
|
||||
selectedNode,
|
||||
handleUpdateNode,
|
||||
setEdges,
|
||||
setIsOpen,
|
||||
setSelectedNode,
|
||||
}: {
|
||||
selectedNode: any;
|
||||
handleUpdateNode: (node: any) => void;
|
||||
setEdges: any;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setSelectedNode: Dispatch<SetStateAction<any>>;
|
||||
}) {
|
||||
const [delay, setDelay] = useState<DelayType>({
|
||||
value: 1,
|
||||
unit: DelayUnitEnum.SECONDS,
|
||||
description: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedNode?.data?.delay) {
|
||||
setDelay(selectedNode.data.delay);
|
||||
}
|
||||
}, [selectedNode]);
|
||||
|
||||
const handleSave = () => {
|
||||
handleUpdateNode({
|
||||
...selectedNode,
|
||||
data: {
|
||||
...selectedNode.data,
|
||||
delay,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setEdges((edges: any) => {
|
||||
return edges.filter(
|
||||
(edge: any) => edge.source !== selectedNode.id && edge.target !== selectedNode.id
|
||||
);
|
||||
});
|
||||
|
||||
setIsOpen(false);
|
||||
setSelectedNode(null);
|
||||
};
|
||||
|
||||
const getUnitLabel = (unit: DelayUnitEnum) => {
|
||||
const units = {
|
||||
[DelayUnitEnum.SECONDS]: "Seconds",
|
||||
[DelayUnitEnum.MINUTES]: "Minutes",
|
||||
[DelayUnitEnum.HOURS]: "Hours",
|
||||
[DelayUnitEnum.DAYS]: "Days",
|
||||
};
|
||||
return units[unit] || unit;
|
||||
};
|
||||
|
||||
const getTimeDescription = () => {
|
||||
const value = delay.value || 0;
|
||||
|
||||
if (value <= 0) return "Invalid time";
|
||||
|
||||
if (value === 1) {
|
||||
return `1 ${getUnitLabel(delay.unit).slice(0, -1)}`;
|
||||
}
|
||||
|
||||
return `${value} ${getUnitLabel(delay.unit)}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="p-4 border-b border-neutral-700 flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-md font-medium text-neutral-200">Delay Duration</h3>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs bg-yellow-900/20 text-yellow-400 border-yellow-700/50"
|
||||
>
|
||||
{getTimeDescription().toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
<Select
|
||||
value={delay.unit}
|
||||
onValueChange={(value) =>
|
||||
setDelay({
|
||||
...delay,
|
||||
unit: value as DelayUnitEnum,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[120px] h-8 bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||
<SelectValue placeholder="Unit" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||
<SelectItem value={DelayUnitEnum.SECONDS}>Seconds</SelectItem>
|
||||
<SelectItem value={DelayUnitEnum.MINUTES}>Minutes</SelectItem>
|
||||
<SelectItem value={DelayUnitEnum.HOURS}>Hours</SelectItem>
|
||||
<SelectItem value={DelayUnitEnum.DAYS}>Days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 min-h-0">
|
||||
<div className="grid gap-4">
|
||||
<div className="p-3 rounded-md bg-yellow-900/10 border border-yellow-700/30 mb-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="bg-yellow-900/50 rounded-full p-1.5 flex-shrink-0">
|
||||
<Clock size={18} className="text-yellow-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium text-neutral-200">Time Delay</h3>
|
||||
<p className="text-sm text-neutral-400 mt-1">
|
||||
Pause workflow execution for a specified amount of time
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="delay-value">Delay Value</Label>
|
||||
<Input
|
||||
id="delay-value"
|
||||
type="number"
|
||||
min="1"
|
||||
className="bg-neutral-700 border-neutral-600"
|
||||
value={delay.value}
|
||||
onChange={(e) =>
|
||||
setDelay({
|
||||
...delay,
|
||||
value: parseInt(e.target.value) || 1,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="delay-description">Description (optional)</Label>
|
||||
<Textarea
|
||||
id="delay-description"
|
||||
className="bg-neutral-700 border-neutral-600 min-h-[100px] resize-none"
|
||||
value={delay.description}
|
||||
onChange={(e) =>
|
||||
setDelay({
|
||||
...delay,
|
||||
description: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="Add a description for this delay"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{delay.value > 0 ? (
|
||||
<div className="rounded-md bg-neutral-700/50 border border-neutral-600 p-3 mt-2">
|
||||
<div className="text-sm font-medium text-neutral-400 mb-1">Preview</div>
|
||||
<div className="flex items-start gap-2 p-2 rounded-md bg-neutral-800/70">
|
||||
<div className="rounded-full bg-yellow-900/30 p-1.5 mt-0.5">
|
||||
<HourglassIcon size={15} className="text-yellow-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm text-yellow-400 font-medium">
|
||||
{getTimeDescription()} delay
|
||||
</span>
|
||||
{delay.description && (
|
||||
<span className="text-xs text-neutral-400 mt-1">
|
||||
{delay.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md bg-neutral-700/30 border border-neutral-600/50 p-4 flex flex-col items-center justify-center text-center">
|
||||
<AlertCircle className="h-6 w-6 text-neutral-500 mb-2" />
|
||||
<p className="text-neutral-400 text-sm">Please set a valid delay time</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-neutral-700 flex-shrink-0">
|
||||
<div className="flex gap-2 justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-red-700/50 bg-red-900/20 text-red-400 hover:bg-red-900/30 hover:text-red-300"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete Node
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-yellow-700 hover:bg-yellow-600 text-white flex items-center gap-2"
|
||||
onClick={handleSave}
|
||||
>
|
||||
<Save size={16} />
|
||||
Save Delay
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @author: Victor Calazans - Implementation of Delay node functionality │
|
||||
│ @file: /app/agents/workflows/nodes/components/delay/DelayNode.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Delay node developed by: Victor Calazans │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Delay node implementation date: May 17, 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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Handle, NodeProps, Position, useEdges } from "@xyflow/react";
|
||||
import { Clock, ArrowRight, Timer } from "lucide-react";
|
||||
import { DelayType } from "../../nodeFunctions";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { BaseNode } from "../../BaseNode";
|
||||
|
||||
export function DelayNode(props: NodeProps) {
|
||||
const { selected, data } = props;
|
||||
|
||||
const edges = useEdges();
|
||||
const isExecuting = data.isExecuting as boolean | undefined;
|
||||
|
||||
const isHandleConnected = (handleId: string) => {
|
||||
return edges.some(
|
||||
(edge) => edge.source === props.id && edge.sourceHandle === handleId
|
||||
);
|
||||
};
|
||||
|
||||
const isBottomHandleConnected = isHandleConnected("bottom-handle");
|
||||
|
||||
const delay = data.delay as DelayType | undefined;
|
||||
|
||||
const getUnitLabel = (unit: string) => {
|
||||
switch (unit) {
|
||||
case 'seconds':
|
||||
return 'Seconds';
|
||||
case 'minutes':
|
||||
return 'Minutes';
|
||||
case 'hours':
|
||||
return 'Hours';
|
||||
case 'days':
|
||||
return 'Days';
|
||||
default:
|
||||
return unit;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseNode hasTarget={true} selected={selected || false} borderColor="yellow" isExecuting={isExecuting}>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-yellow-900/40 shadow-sm">
|
||||
<Clock className="h-5 w-5 text-yellow-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-medium text-yellow-400">
|
||||
{data.label as string}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{delay ? (
|
||||
<div className="mb-3 rounded-lg border border-yellow-700/40 bg-yellow-950/10 p-3 transition-all duration-200 hover:border-yellow-600/50 hover:bg-yellow-900/10">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center">
|
||||
<Timer className="h-4 w-4 text-yellow-400" />
|
||||
<span className="ml-1.5 font-medium text-white">Delay</span>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="px-1.5 py-0 text-xs bg-yellow-900/30 text-yellow-400 border-yellow-700/40"
|
||||
>
|
||||
{getUnitLabel(delay.unit)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center">
|
||||
<span className="text-lg font-semibold text-yellow-300">{delay.value}</span>
|
||||
<span className="ml-1 text-sm text-neutral-400">{delay.unit}</span>
|
||||
</div>
|
||||
|
||||
{delay.description && (
|
||||
<p className="mt-2 text-xs text-neutral-400 line-clamp-2">
|
||||
{delay.description.slice(0, 80)} {delay.description.length > 80 && '...'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-3 flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-yellow-700/40 bg-yellow-950/10 p-5 text-center transition-all duration-200 hover:border-yellow-600/50 hover:bg-yellow-900/20">
|
||||
<Clock className="h-8 w-8 text-yellow-700/50 mb-2" />
|
||||
<p className="text-yellow-400">No delay configured</p>
|
||||
<p className="mt-1 text-xs text-neutral-500">Click to configure</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 flex items-center justify-end text-sm text-neutral-400 transition-colors">
|
||||
<div className="flex items-center space-x-1 rounded-md py-1 px-2">
|
||||
<span>Next step</span>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<Handle
|
||||
className={cn(
|
||||
"!w-3 !h-3 !rounded-full transition-all duration-300",
|
||||
isBottomHandleConnected ? "!bg-yellow-500 !border-yellow-400" : "!bg-neutral-400 !border-neutral-500",
|
||||
selected && isBottomHandleConnected && "!bg-yellow-400 !border-yellow-300"
|
||||
)}
|
||||
style={{
|
||||
right: "-8px",
|
||||
top: "calc(100% - 25px)",
|
||||
}}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="bottom-handle"
|
||||
/>
|
||||
</div>
|
||||
</BaseNode>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/message/MessageForm.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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* eslint-disable jsx-a11y/alt-text */
|
||||
import { useEdges, useNodes } from "@xyflow/react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { listAgents } from "@/services/agentService";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
MessageSquare,
|
||||
Save,
|
||||
Text,
|
||||
Image,
|
||||
File,
|
||||
Video,
|
||||
AlertCircle
|
||||
} from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
function MessageForm({
|
||||
selectedNode,
|
||||
handleUpdateNode,
|
||||
setEdges,
|
||||
setIsOpen,
|
||||
setSelectedNode,
|
||||
}: {
|
||||
selectedNode: any;
|
||||
handleUpdateNode: any;
|
||||
setEdges: any;
|
||||
setIsOpen: any;
|
||||
setSelectedNode: any;
|
||||
}) {
|
||||
const [node, setNode] = useState(selectedNode);
|
||||
const [messageType, setMessageType] = useState("text");
|
||||
const [content, setContent] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [allAgents, setAllAgents] = useState<Agent[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const edges = useEdges();
|
||||
const nodes = useNodes();
|
||||
|
||||
const connectedNode = useMemo(() => {
|
||||
const edge = edges.find((edge) => edge.source === selectedNode.id);
|
||||
if (!edge) return null;
|
||||
const node = nodes.find((node) => node.id === edge.target);
|
||||
return node || null;
|
||||
}, [edges, nodes, selectedNode.id]);
|
||||
|
||||
const user = typeof window !== "undefined" ? JSON.parse(localStorage.getItem("user") || '{}') : {};
|
||||
const clientId = user?.client_id || "";
|
||||
|
||||
const currentAgent = typeof window !== "undefined" ?
|
||||
JSON.parse(localStorage.getItem("current_workflow_agent") || '{}') : {};
|
||||
const currentAgentId = currentAgent?.id;
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedNode) {
|
||||
setNode(selectedNode);
|
||||
setMessageType(selectedNode.data.message?.type || "text");
|
||||
setContent(selectedNode.data.message?.content || "");
|
||||
}
|
||||
}, [selectedNode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clientId) return;
|
||||
setLoading(true);
|
||||
listAgents(clientId)
|
||||
.then((res) => {
|
||||
const filteredAgents = res.data.filter((agent: Agent) => agent.id !== currentAgentId);
|
||||
setAllAgents(filteredAgents);
|
||||
setAgents(filteredAgents);
|
||||
})
|
||||
.catch((error) => console.error("Error loading agents:", error))
|
||||
.finally(() => setLoading(false));
|
||||
}, [clientId, currentAgentId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchTerm.trim() === "") {
|
||||
setAgents(allAgents);
|
||||
} else {
|
||||
const filtered = allAgents.filter((agent) =>
|
||||
agent.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
setAgents(filtered);
|
||||
}
|
||||
}, [searchTerm, allAgents]);
|
||||
|
||||
const handleDeleteEdge = useCallback(() => {
|
||||
const id = edges.find((edge: any) => edge.source === selectedNode.id)?.id;
|
||||
setEdges((edges: any) => {
|
||||
const left = edges.filter((edge: any) => edge.id !== id);
|
||||
return left;
|
||||
});
|
||||
}, [nodes, edges, selectedNode, setEdges]);
|
||||
|
||||
const handleSelectAgent = (agent: Agent) => {
|
||||
const updatedNode = {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
agent,
|
||||
},
|
||||
};
|
||||
setNode(updatedNode);
|
||||
handleUpdateNode(updatedNode);
|
||||
};
|
||||
|
||||
const getAgentTypeName = (type: string) => {
|
||||
const agentTypes: Record<string, string> = {
|
||||
llm: "LLM Agent",
|
||||
a2a: "A2A Agent",
|
||||
sequential: "Sequential Agent",
|
||||
parallel: "Parallel Agent",
|
||||
loop: "Loop Agent",
|
||||
workflow: "Workflow Agent",
|
||||
task: "Task Agent",
|
||||
};
|
||||
return agentTypes[type] || type;
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const updatedNode = {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
message: {
|
||||
type: messageType,
|
||||
content,
|
||||
},
|
||||
},
|
||||
};
|
||||
setNode(updatedNode);
|
||||
handleUpdateNode(updatedNode);
|
||||
};
|
||||
|
||||
const messageTypeInfo = {
|
||||
text: {
|
||||
icon: <Text className="h-5 w-5 text-orange-400" />,
|
||||
name: "Text Message",
|
||||
description: "Simple text message",
|
||||
color: "bg-orange-900/30 border-orange-700/50",
|
||||
},
|
||||
image: {
|
||||
icon: <Image className="h-5 w-5 text-blue-400" />,
|
||||
name: "Image",
|
||||
description: "Image URL or base64",
|
||||
color: "bg-blue-900/30 border-blue-700/50",
|
||||
},
|
||||
file: {
|
||||
icon: <File className="h-5 w-5 text-emerald-400" />,
|
||||
name: "File",
|
||||
description: "File URL or base64",
|
||||
color: "bg-emerald-900/30 border-emerald-700/50",
|
||||
},
|
||||
video: {
|
||||
icon: <Video className="h-5 w-5 text-purple-400" />,
|
||||
name: "Video",
|
||||
description: "Video URL or base64",
|
||||
color: "bg-purple-900/30 border-purple-700/50",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="p-4 border-b border-neutral-700 flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-md font-medium text-neutral-200">Message Type</h3>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs bg-orange-900/20 text-orange-400 border-orange-700/50"
|
||||
>
|
||||
{messageType === "text" ? "TEXT" : messageType.toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
<Select
|
||||
value={messageType}
|
||||
onValueChange={setMessageType}
|
||||
>
|
||||
<SelectTrigger className="w-[120px] h-8 bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||
<SelectValue placeholder="Select type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||
<SelectItem value="text">Text</SelectItem>
|
||||
{/* Other options can be enabled in the future */}
|
||||
{/* <SelectItem value="image">Image</SelectItem>
|
||||
<SelectItem value="file">File</SelectItem>
|
||||
<SelectItem value="video">Video</SelectItem> */}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 min-h-0">
|
||||
<div className="grid gap-4">
|
||||
<div className="p-3 rounded-md bg-orange-900/10 border border-orange-700/30 mb-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="bg-orange-900/50 rounded-full p-1.5 flex-shrink-0">
|
||||
<MessageSquare size={18} className="text-orange-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium text-neutral-200">{messageTypeInfo.text.name}</h3>
|
||||
<p className="text-sm text-neutral-400 mt-1">{messageTypeInfo.text.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="content">Message Content</Label>
|
||||
<Textarea
|
||||
id="content"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder="Type your message here..."
|
||||
className="min-h-[150px] bg-neutral-700 border-neutral-600 resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{content.trim() !== "" ? (
|
||||
<div className="rounded-md bg-neutral-700/50 border border-neutral-600 p-3 mt-2">
|
||||
<div className="text-sm font-medium text-neutral-400 mb-1">Preview</div>
|
||||
<div className="flex items-start gap-2 p-2 rounded-md bg-neutral-800/70">
|
||||
<div className="rounded-full bg-orange-900/30 p-1.5 mt-0.5">
|
||||
<MessageSquare size={15} className="text-orange-400" />
|
||||
</div>
|
||||
<div className="text-sm text-neutral-300 whitespace-pre-wrap">{content}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md bg-neutral-700/30 border border-neutral-600/50 p-4 flex flex-col items-center justify-center text-center">
|
||||
<AlertCircle className="h-6 w-6 text-neutral-500 mb-2" />
|
||||
<p className="text-neutral-400 text-sm">Your message will appear here</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-neutral-700 flex-shrink-0">
|
||||
<Button
|
||||
className="w-full bg-orange-700 hover:bg-orange-600 text-white flex items-center gap-2 justify-center"
|
||||
onClick={handleSave}
|
||||
>
|
||||
<Save size={16} />
|
||||
Save Message
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { MessageForm };
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/message/MessageNode.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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Handle, NodeProps, Position, useEdges } from "@xyflow/react";
|
||||
import { MessageCircle, Text, Image, File, Video, ArrowRight } from "lucide-react";
|
||||
import { MessageType, MessageTypeEnum } from "../../nodeFunctions";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { BaseNode } from "../../BaseNode";
|
||||
|
||||
export function MessageNode(props: NodeProps) {
|
||||
const { selected, data } = props;
|
||||
const edges = useEdges();
|
||||
const isExecuting = data.isExecuting as boolean | undefined;
|
||||
|
||||
const isHandleConnected = (handleId: string) => {
|
||||
return edges.some(
|
||||
(edge) => edge.source === props.id && edge.sourceHandle === handleId
|
||||
);
|
||||
};
|
||||
|
||||
const isBottomHandleConnected = isHandleConnected("bottom-handle");
|
||||
|
||||
const message = data.message as MessageType | undefined;
|
||||
|
||||
const getMessageTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case MessageTypeEnum.TEXT:
|
||||
return <Text className="h-4 w-4 text-orange-400" />;
|
||||
case "image":
|
||||
return <Image className="h-4 w-4 text-blue-400" />;
|
||||
case "file":
|
||||
return <File className="h-4 w-4 text-emerald-400" />;
|
||||
case "video":
|
||||
return <Video className="h-4 w-4 text-purple-400" />;
|
||||
default:
|
||||
return <MessageCircle className="h-4 w-4 text-orange-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getMessageTypeColor = (type: string) => {
|
||||
switch (type) {
|
||||
case MessageTypeEnum.TEXT:
|
||||
return 'bg-orange-900/30 text-orange-400 border-orange-700/40';
|
||||
case "image":
|
||||
return 'bg-blue-900/30 text-blue-400 border-blue-700/40';
|
||||
case "file":
|
||||
return 'bg-emerald-900/30 text-emerald-400 border-emerald-700/40';
|
||||
case "video":
|
||||
return 'bg-purple-900/30 text-purple-400 border-purple-700/40';
|
||||
default:
|
||||
return 'bg-orange-900/30 text-orange-400 border-orange-700/40';
|
||||
}
|
||||
};
|
||||
|
||||
const getMessageTypeName = (type: string) => {
|
||||
const messageTypes: Record<string, string> = {
|
||||
text: "Text Message",
|
||||
image: "Image",
|
||||
file: "File",
|
||||
video: "Video",
|
||||
};
|
||||
return messageTypes[type] || type;
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseNode hasTarget={true} selected={selected || false} borderColor="orange" isExecuting={isExecuting}>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-orange-900/40 shadow-sm">
|
||||
<MessageCircle className="h-5 w-5 text-orange-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-medium text-orange-400">
|
||||
{data.label as string}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message ? (
|
||||
<div className="mb-3 rounded-lg border border-orange-700/40 bg-orange-950/10 p-3 transition-all duration-200 hover:border-orange-600/50 hover:bg-orange-900/10">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center">
|
||||
{getMessageTypeIcon(message.type)}
|
||||
<span className="ml-1.5 font-medium text-white">{getMessageTypeName(message.type)}</span>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn("px-1.5 py-0 text-xs", getMessageTypeColor(message.type))}
|
||||
>
|
||||
{message.type}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{message.content && (
|
||||
<p className="mt-2 text-xs text-neutral-400 line-clamp-2">
|
||||
{message.content.slice(0, 80)} {message.content.length > 80 && '...'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-3 flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-orange-700/40 bg-orange-950/10 p-5 text-center transition-all duration-200 hover:border-orange-600/50 hover:bg-orange-900/20">
|
||||
<MessageCircle className="h-8 w-8 text-orange-700/50 mb-2" />
|
||||
<p className="text-orange-400">No message configured</p>
|
||||
<p className="mt-1 text-xs text-neutral-500">Click to configure</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 flex items-center justify-end text-sm text-neutral-400 transition-colors">
|
||||
<div className="flex items-center space-x-1 rounded-md py-1 px-2">
|
||||
<span>Next step</span>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<Handle
|
||||
className={cn(
|
||||
"!w-3 !h-3 !rounded-full transition-all duration-300",
|
||||
isBottomHandleConnected ? "!bg-orange-500 !border-orange-400" : "!bg-neutral-400 !border-neutral-500",
|
||||
selected && isBottomHandleConnected && "!bg-orange-400 !border-orange-300"
|
||||
)}
|
||||
style={{
|
||||
right: "-8px",
|
||||
top: "calc(100% - 25px)",
|
||||
}}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="bottom-handle"
|
||||
/>
|
||||
</div>
|
||||
</BaseNode>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/start/StartNode.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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Handle, Node, NodeProps, Position, useEdges } from "@xyflow/react";
|
||||
import { Zap, ArrowRight } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { BaseNode } from "../../BaseNode";
|
||||
|
||||
export type StartNodeType = Node<
|
||||
{
|
||||
label?: string;
|
||||
},
|
||||
"start-node"
|
||||
>;
|
||||
|
||||
export function StartNode(props: NodeProps) {
|
||||
const { selected, data } = props;
|
||||
const edges = useEdges();
|
||||
const isExecuting = data.isExecuting as boolean | undefined;
|
||||
|
||||
const isSourceHandleConnected = edges.some(
|
||||
(edge) => edge.source === props.id
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseNode hasTarget={true} selected={selected || false} borderColor="emerald" isExecuting={isExecuting}>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-emerald-900/40 shadow-sm">
|
||||
<Zap className="h-5 w-5 text-emerald-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-medium text-emerald-400">Start</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3 rounded-lg border border-emerald-700/40 bg-emerald-950/10 p-3 transition-all duration-200 hover:border-emerald-600/50 hover:bg-emerald-900/10">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center">
|
||||
<span className="font-medium text-white">Input: User content</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-2 text-xs text-neutral-400">
|
||||
The workflow begins when a user sends a message to the agent
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center justify-end text-sm text-neutral-400 transition-colors">
|
||||
<div className="flex items-center space-x-1 rounded-md py-1 px-2">
|
||||
<span>Next step</span>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<Handle
|
||||
className={cn(
|
||||
"!w-3 !h-3 !rounded-full transition-all duration-300",
|
||||
isSourceHandleConnected ? "!bg-emerald-500 !border-emerald-400" : "!bg-neutral-400 !border-neutral-500",
|
||||
selected && isSourceHandleConnected && "!bg-emerald-400 !border-emerald-300"
|
||||
)}
|
||||
style={{
|
||||
right: "-8px",
|
||||
top: "calc(100% - 25px)",
|
||||
}}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
/>
|
||||
</div>
|
||||
</BaseNode>
|
||||
);
|
||||
}
|
||||
109
frontend/app/agents/workflows/nodes/index.ts
Normal file
109
frontend/app/agents/workflows/nodes/index.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @author: Victor Calazans - Implementation of Delay node type │
|
||||
│ @file: /app/agents/workflows/nodes/index.ts │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Delay node functionality developed by: Victor Calazans │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Delay implementation date: May 17, 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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
import type { NodeTypes, BuiltInNode, Node } from "@xyflow/react";
|
||||
|
||||
import { ConditionNode } from "./components/condition/ConditionNode";
|
||||
import { AgentNode } from "./components/agent/AgentNode";
|
||||
import { StartNode, StartNodeType } from "./components/start/StartNode";
|
||||
import { MessageNode } from "./components/message/MessageNode";
|
||||
import { DelayNode } from "./components/delay/DelayNode";
|
||||
|
||||
import "./style.css";
|
||||
import {
|
||||
ConditionType,
|
||||
MessageType,
|
||||
DelayType,
|
||||
} from "./nodeFunctions";
|
||||
import { Agent } from "@/types/agent";
|
||||
|
||||
type AgentNodeType = Node<
|
||||
{
|
||||
label?: string;
|
||||
agent?: Agent;
|
||||
},
|
||||
"agent-node"
|
||||
>;
|
||||
|
||||
type MessageNodeType = Node<
|
||||
{
|
||||
label?: string;
|
||||
message?: MessageType;
|
||||
},
|
||||
"message-node"
|
||||
>;
|
||||
|
||||
type DelayNodeType = Node<
|
||||
{
|
||||
label?: string;
|
||||
delay?: DelayType;
|
||||
},
|
||||
"delay-node"
|
||||
>;
|
||||
|
||||
type ConditionNodeType = Node<
|
||||
{
|
||||
label?: string;
|
||||
integration?: string;
|
||||
icon?: string;
|
||||
conditions?: ConditionType[];
|
||||
},
|
||||
"condition-node"
|
||||
>;
|
||||
|
||||
export type AppNode =
|
||||
| BuiltInNode
|
||||
| StartNodeType
|
||||
| AgentNodeType
|
||||
| ConditionNodeType
|
||||
| MessageNodeType
|
||||
| DelayNodeType;
|
||||
|
||||
export type NodeType = AppNode["type"];
|
||||
|
||||
export const initialNodes: AppNode[] = [
|
||||
{
|
||||
id: "start-node",
|
||||
type: "start-node",
|
||||
position: { x: -100, y: 100 },
|
||||
data: {
|
||||
label: "Start",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const nodeTypes = {
|
||||
"start-node": StartNode,
|
||||
"agent-node": AgentNode,
|
||||
"message-node": MessageNode,
|
||||
"condition-node": ConditionNode,
|
||||
"delay-node": DelayNode,
|
||||
} satisfies NodeTypes;
|
||||
65
frontend/app/agents/workflows/nodes/nodeFunctions.ts
Normal file
65
frontend/app/agents/workflows/nodes/nodeFunctions.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @author: Victor Calazans - Implementation of Delay types │
|
||||
│ @file: /app/agents/workflows/nodes/nodeFunctions.ts │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Delay node functionality developed by: Victor Calazans │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Delay implementation date: May 17, 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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable no-unused-vars */
|
||||
export enum ConditionTypeEnum {
|
||||
PREVIOUS_OUTPUT = "previous-output",
|
||||
}
|
||||
|
||||
export enum MessageTypeEnum {
|
||||
TEXT = "text",
|
||||
}
|
||||
|
||||
export enum DelayUnitEnum {
|
||||
SECONDS = "seconds",
|
||||
MINUTES = "minutes",
|
||||
HOURS = "hours",
|
||||
DAYS = "days",
|
||||
}
|
||||
|
||||
export type MessageType = {
|
||||
type: MessageTypeEnum;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type DelayType = {
|
||||
value: number;
|
||||
unit: DelayUnitEnum;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type ConditionType = {
|
||||
id: string;
|
||||
type: ConditionTypeEnum;
|
||||
data?: any;
|
||||
};
|
||||
|
||||
54
frontend/app/agents/workflows/nodes/style.css
Normal file
54
frontend/app/agents/workflows/nodes/style.css
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/style.css │
|
||||
│ 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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* .react-flow__handle {
|
||||
background-color: #8492A6;
|
||||
border-radius: 50%;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
} */
|
||||
|
||||
/* .react-flow__handle-right {
|
||||
right: -6px;
|
||||
top: 88%;
|
||||
transform: translateY(-75%);
|
||||
background-color: #f5f5f5 !important;
|
||||
border: 3px solid #8492A6 !important;
|
||||
} */
|
||||
|
||||
/* .react-flow__handle-left {
|
||||
left: 0px;
|
||||
top: 40%;
|
||||
width: 60px;
|
||||
border-radius: 0;
|
||||
height: 50%;
|
||||
transform: translateY(-75%);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
} */
|
||||
218
frontend/app/agents/workflows/page.tsx
Normal file
218
frontend/app/agents/workflows/page.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/page.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 { useEffect, useState, useRef, Suspense } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import Canva from "./Canva";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { getAgent, updateAgent } from "@/services/agentService";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft, Save, Download, PlayIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ReactFlowProvider } from "@xyflow/react";
|
||||
import { DnDProvider } from "@/contexts/DnDContext";
|
||||
import { NodeDataProvider } from "@/contexts/NodeDataContext";
|
||||
import { SourceClickProvider } from "@/contexts/SourceClickContext";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { AgentTestChatModal } from "./nodes/components/agent/AgentTestChatModal";
|
||||
|
||||
function WorkflowsContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const agentId = searchParams.get("agentId");
|
||||
const [agent, setAgent] = useState<Agent | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const canvaRef = useRef<any>(null);
|
||||
const { toast } = useToast();
|
||||
const [isTestModalOpen, setIsTestModalOpen] = useState(false);
|
||||
|
||||
const user =
|
||||
typeof window !== "undefined"
|
||||
? JSON.parse(localStorage.getItem("user") || "{}")
|
||||
: {};
|
||||
const clientId = user?.client_id || "";
|
||||
|
||||
useEffect(() => {
|
||||
if (agentId && clientId) {
|
||||
setLoading(true);
|
||||
getAgent(agentId, clientId)
|
||||
.then((res) => {
|
||||
setAgent(res.data);
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(
|
||||
"current_workflow_agent",
|
||||
JSON.stringify(res.data)
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error loading agent:", err);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}, [agentId, clientId]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.removeItem("current_workflow_agent");
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSaveWorkflow = async () => {
|
||||
if (!agent || !canvaRef.current) return;
|
||||
|
||||
try {
|
||||
const { nodes, edges } = canvaRef.current.getFlowData();
|
||||
|
||||
const workflow = {
|
||||
nodes,
|
||||
edges,
|
||||
};
|
||||
|
||||
await updateAgent(agent.id, {
|
||||
...agent,
|
||||
config: {
|
||||
...agent.config,
|
||||
workflow,
|
||||
},
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "Workflow saved",
|
||||
description: "The changes were saved successfully",
|
||||
});
|
||||
|
||||
canvaRef.current.setHasChanges(false);
|
||||
} catch (error) {
|
||||
console.error("Error saving workflow:", error);
|
||||
toast({
|
||||
title: "Error saving workflow",
|
||||
description: "Unable to save the changes",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="w-full h-screen bg-[#121212] flex items-center justify-center">
|
||||
<div className="text-white text-xl">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-screen flex flex-col" data-workflow-page="true">
|
||||
{/* Header with controls */}
|
||||
<div className="w-full bg-[#121212] py-4 px-6 z-10 flex items-center justify-between border-b border-neutral-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="/agents">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-neutral-800 border-neutral-700 text-neutral-200 hover:bg-neutral-700"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Agents
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{agent && (
|
||||
<div className="bg-neutral-800 px-4 py-2 rounded-md">
|
||||
<h2 className="text-neutral-200 font-medium">
|
||||
{agent.name} -{" "}
|
||||
<span className="text-neutral-400 text-sm">{agent.type}</span>
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-neutral-800 border-neutral-700 text-neutral-200 hover:bg-neutral-700"
|
||||
onClick={handleSaveWorkflow}
|
||||
>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
Save
|
||||
</Button>
|
||||
{agent && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-green-800 border-green-700 text-green-200 hover:bg-green-700"
|
||||
onClick={() => setIsTestModalOpen(true)}
|
||||
>
|
||||
<PlayIcon className="h-4 w-4 mr-2" />
|
||||
Test Workflow
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content area */}
|
||||
<div className="flex-1 relative overflow-hidden">
|
||||
{agent && isTestModalOpen && (
|
||||
<AgentTestChatModal
|
||||
open={isTestModalOpen}
|
||||
onOpenChange={setIsTestModalOpen}
|
||||
agent={agent}
|
||||
canvasRef={canvaRef} // Pass the canvas reference to allow visualization of running nodes
|
||||
/>
|
||||
)}
|
||||
|
||||
<NodeDataProvider>
|
||||
<SourceClickProvider>
|
||||
<DnDProvider>
|
||||
<ReactFlowProvider>
|
||||
<Canva agent={agent} ref={canvaRef} data-canvas-ref="true" />
|
||||
</ReactFlowProvider>
|
||||
</DnDProvider>
|
||||
</SourceClickProvider>
|
||||
</NodeDataProvider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WorkflowsPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="w-full h-screen bg-[#121212] flex items-center justify-center">
|
||||
<div className="text-white text-xl">Loading...</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<WorkflowsContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
199
frontend/app/agents/workflows/utils.ts
Normal file
199
frontend/app/agents/workflows/utils.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/utils.ts │
|
||||
│ 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. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
import { Node, NodePositionChange, XYPosition } from "@xyflow/react";
|
||||
|
||||
type GetHelperLinesResult = {
|
||||
horizontal?: number;
|
||||
vertical?: number;
|
||||
snapPosition: Partial<XYPosition>;
|
||||
};
|
||||
|
||||
// this utility function can be called with a position change (inside onNodesChange)
|
||||
// it checks all other nodes and calculated the helper line positions and the position where the current node should snap to
|
||||
export function getHelperLines(
|
||||
change: NodePositionChange,
|
||||
nodes: Node[],
|
||||
distance = 5,
|
||||
): GetHelperLinesResult {
|
||||
const defaultResult = {
|
||||
horizontal: undefined,
|
||||
vertical: undefined,
|
||||
snapPosition: { x: undefined, y: undefined },
|
||||
};
|
||||
const nodeA = nodes.find((node) => node.id === change.id);
|
||||
|
||||
if (!nodeA || !change.position) {
|
||||
return defaultResult;
|
||||
}
|
||||
|
||||
const nodeABounds = {
|
||||
left: change.position.x,
|
||||
right: change.position.x + (nodeA.measured?.width ?? 0),
|
||||
top: change.position.y,
|
||||
bottom: change.position.y + (nodeA.measured?.height ?? 0),
|
||||
width: nodeA.measured?.width ?? 0,
|
||||
height: nodeA.measured?.height ?? 0,
|
||||
};
|
||||
|
||||
let horizontalDistance = distance;
|
||||
let verticalDistance = distance;
|
||||
|
||||
return nodes
|
||||
.filter((node) => node.id !== nodeA.id)
|
||||
.reduce<GetHelperLinesResult>((result, nodeB) => {
|
||||
const nodeBBounds = {
|
||||
left: nodeB.position.x,
|
||||
right: nodeB.position.x + (nodeB.measured?.width ?? 0),
|
||||
top: nodeB.position.y,
|
||||
bottom: nodeB.position.y + (nodeB.measured?.height ?? 0),
|
||||
width: nodeB.measured?.width ?? 0,
|
||||
height: nodeB.measured?.height ?? 0,
|
||||
};
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A |
|
||||
// |___________|
|
||||
// |
|
||||
// |
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | B |
|
||||
// |___________|
|
||||
const distanceLeftLeft = Math.abs(nodeABounds.left - nodeBBounds.left);
|
||||
|
||||
if (distanceLeftLeft < verticalDistance) {
|
||||
result.snapPosition.x = nodeBBounds.left;
|
||||
result.vertical = nodeBBounds.left;
|
||||
verticalDistance = distanceLeftLeft;
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A |
|
||||
// |___________|
|
||||
// |
|
||||
// |
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | B |
|
||||
// |___________|
|
||||
const distanceRightRight = Math.abs(
|
||||
nodeABounds.right - nodeBBounds.right,
|
||||
);
|
||||
|
||||
if (distanceRightRight < verticalDistance) {
|
||||
result.snapPosition.x = nodeBBounds.right - nodeABounds.width;
|
||||
result.vertical = nodeBBounds.right;
|
||||
verticalDistance = distanceRightRight;
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A |
|
||||
// |___________|
|
||||
// |
|
||||
// |
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | B |
|
||||
// |___________|
|
||||
const distanceLeftRight = Math.abs(nodeABounds.left - nodeBBounds.right);
|
||||
|
||||
if (distanceLeftRight < verticalDistance) {
|
||||
result.snapPosition.x = nodeBBounds.right;
|
||||
result.vertical = nodeBBounds.right;
|
||||
verticalDistance = distanceLeftRight;
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A |
|
||||
// |___________|
|
||||
// |
|
||||
// |
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | B |
|
||||
// |___________|
|
||||
const distanceRightLeft = Math.abs(nodeABounds.right - nodeBBounds.left);
|
||||
|
||||
if (distanceRightLeft < verticalDistance) {
|
||||
result.snapPosition.x = nodeBBounds.left - nodeABounds.width;
|
||||
result.vertical = nodeBBounds.left;
|
||||
verticalDistance = distanceRightLeft;
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|‾‾‾‾‾|‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A | | B |
|
||||
// |___________| |___________|
|
||||
const distanceTopTop = Math.abs(nodeABounds.top - nodeBBounds.top);
|
||||
|
||||
if (distanceTopTop < horizontalDistance) {
|
||||
result.snapPosition.y = nodeBBounds.top;
|
||||
result.horizontal = nodeBBounds.top;
|
||||
horizontalDistance = distanceTopTop;
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A |
|
||||
// |___________|_________________
|
||||
// | |
|
||||
// | B |
|
||||
// |___________|
|
||||
const distanceBottomTop = Math.abs(nodeABounds.bottom - nodeBBounds.top);
|
||||
|
||||
if (distanceBottomTop < horizontalDistance) {
|
||||
result.snapPosition.y = nodeBBounds.top - nodeABounds.height;
|
||||
result.horizontal = nodeBBounds.top;
|
||||
horizontalDistance = distanceBottomTop;
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾| |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A | | B |
|
||||
// |___________|_____|___________|
|
||||
const distanceBottomBottom = Math.abs(
|
||||
nodeABounds.bottom - nodeBBounds.bottom,
|
||||
);
|
||||
|
||||
if (distanceBottomBottom < horizontalDistance) {
|
||||
result.snapPosition.y = nodeBBounds.bottom - nodeABounds.height;
|
||||
result.horizontal = nodeBBounds.bottom;
|
||||
horizontalDistance = distanceBottomBottom;
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | B |
|
||||
// | |
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
||||
// | A |
|
||||
// |___________|
|
||||
const distanceTopBottom = Math.abs(nodeABounds.top - nodeBBounds.bottom);
|
||||
|
||||
if (distanceTopBottom < horizontalDistance) {
|
||||
result.snapPosition.y = nodeBBounds.bottom;
|
||||
result.horizontal = nodeBBounds.bottom;
|
||||
horizontalDistance = distanceTopBottom;
|
||||
}
|
||||
|
||||
return result;
|
||||
}, defaultResult);
|
||||
}
|
||||
Reference in New Issue
Block a user