Commit inicial - upload de todos os arquivos da pasta
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user