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