mirror of https://github.com/kortix-ai/suna.git
338 lines
13 KiB
TypeScript
338 lines
13 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
import { Loader2, Search, Settings2, Sparkles } from 'lucide-react';
|
|
import { DEFAULT_AGENTPRESS_TOOLS, getToolDisplayName } from './tools';
|
|
import { useCreateAgent } from '@/hooks/react-query/agents/use-agents';
|
|
import { MCPConfigurationNew } from './mcp/mcp-configuration-new';
|
|
|
|
interface AgentCreateRequest {
|
|
name: string;
|
|
description: string;
|
|
system_prompt: string;
|
|
configured_mcps: Array<{ name: string; qualifiedName: string; config: any; enabledTools?: string[] }>;
|
|
custom_mcps?: Array<{ name: string; type: 'json' | 'sse'; config: any; enabledTools: string[] }>;
|
|
agentpress_tools: Record<string, { enabled: boolean; description: string }>;
|
|
is_default: boolean;
|
|
}
|
|
|
|
interface CreateAgentDialogProps {
|
|
isOpen: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onAgentCreated?: () => void;
|
|
}
|
|
|
|
const TOOL_CATEGORIES = ['All', 'AI', 'Code', 'Integration', 'Search', 'File', 'Data'];
|
|
|
|
const initialFormData: AgentCreateRequest = {
|
|
name: '',
|
|
description: '',
|
|
system_prompt: 'Describe the agent\'s role, behavior, and expertise...',
|
|
configured_mcps: [],
|
|
custom_mcps: [],
|
|
agentpress_tools: Object.fromEntries(
|
|
Object.entries(DEFAULT_AGENTPRESS_TOOLS).map(([key, value]) => [
|
|
key,
|
|
{ enabled: value.enabled, description: value.description }
|
|
])
|
|
),
|
|
is_default: false,
|
|
};
|
|
|
|
export const CreateAgentDialog = ({ isOpen, onOpenChange, onAgentCreated }: CreateAgentDialogProps) => {
|
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
|
const [selectedCategory, setSelectedCategory] = useState<string>('All');
|
|
const [formData, setFormData] = useState<AgentCreateRequest>(initialFormData);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
const createAgentMutation = useCreateAgent();
|
|
useEffect(() => {
|
|
if (!isOpen) {
|
|
setFormData(initialFormData);
|
|
setSearchQuery('');
|
|
setSelectedCategory('All');
|
|
}
|
|
}, [isOpen]);
|
|
|
|
const handleInputChange = (field: keyof AgentCreateRequest, value: any) => {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
[field]: value
|
|
}));
|
|
};
|
|
|
|
const handleToolToggle = (toolName: string, enabled: boolean) => {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
agentpress_tools: {
|
|
...prev.agentpress_tools,
|
|
[toolName]: {
|
|
...prev.agentpress_tools?.[toolName],
|
|
enabled
|
|
}
|
|
}
|
|
}));
|
|
};
|
|
|
|
const handleMCPConfigurationChange = (mcps: any[]) => {
|
|
// Separate standard and custom MCPs
|
|
const standardMcps = mcps.filter(mcp => !mcp.isCustom);
|
|
const customMcps = mcps.filter(mcp => mcp.isCustom).map(mcp => ({
|
|
name: mcp.name,
|
|
type: mcp.customType as 'json' | 'sse',
|
|
config: mcp.config,
|
|
enabledTools: mcp.enabledTools || []
|
|
}));
|
|
|
|
handleInputChange('configured_mcps', standardMcps);
|
|
handleInputChange('custom_mcps', customMcps);
|
|
};
|
|
|
|
const getSelectedToolsCount = (): number => {
|
|
return Object.values(formData.agentpress_tools).filter(tool => tool.enabled).length;
|
|
};
|
|
|
|
const getFilteredTools = (): Array<[string, any]> => {
|
|
let tools = Object.entries(DEFAULT_AGENTPRESS_TOOLS);
|
|
|
|
if (searchQuery) {
|
|
tools = tools.filter(([toolName, toolInfo]) =>
|
|
getToolDisplayName(toolName).toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
toolInfo.description.toLowerCase().includes(searchQuery.toLowerCase())
|
|
);
|
|
}
|
|
|
|
return tools;
|
|
};
|
|
|
|
const handleSubmit = useCallback(async () => {
|
|
if (!formData.name.trim() || isSubmitting || createAgentMutation.isPending) {
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
|
|
try {
|
|
await createAgentMutation.mutateAsync(formData);
|
|
onOpenChange(false);
|
|
onAgentCreated?.();
|
|
} catch (error) {
|
|
console.error('Error creating agent:', error);
|
|
} finally {
|
|
// Reset the debounce state after a delay
|
|
setTimeout(() => setIsSubmitting(false), 1000);
|
|
}
|
|
}, [formData, isSubmitting, createAgentMutation, onOpenChange, onAgentCreated]);
|
|
|
|
const handleCancel = () => {
|
|
onOpenChange(false);
|
|
};
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-4xl h-[85vh] p-0 gap-0 flex flex-col">
|
|
<DialogHeader className="px-6 py-4 border-b flex-shrink-0">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<DialogTitle className="text-xl font-semibold">
|
|
Create New Agent
|
|
</DialogTitle>
|
|
<DialogDescription className="text-sm mt-1">
|
|
Configure your custom agent with specific tools and instructions
|
|
</DialogDescription>
|
|
</div>
|
|
</div>
|
|
</DialogHeader>
|
|
|
|
<div className="flex-1 w-full overflow-hidden min-h-0">
|
|
<div className="flex w-full h-full">
|
|
<div className="p-6 py-4 w-[40%] space-y-6 overflow-y-auto">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="agent-name" className="text-sm font-medium">
|
|
Agent Name
|
|
</Label>
|
|
<Input
|
|
id="agent-name"
|
|
value={formData.name}
|
|
onChange={(e) => handleInputChange('name', e.target.value)}
|
|
placeholder="e.g., Research Assistant"
|
|
className="h-10"
|
|
disabled={createAgentMutation.isPending}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="agent-description" className="text-sm font-medium">
|
|
Description
|
|
</Label>
|
|
<Input
|
|
id="agent-description"
|
|
value={formData.description}
|
|
onChange={(e) => handleInputChange('description', e.target.value)}
|
|
placeholder="Brief description of the agent"
|
|
className="h-10"
|
|
disabled={createAgentMutation.isPending}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2 flex-1">
|
|
<Label htmlFor="system-instructions" className="text-sm font-medium">
|
|
System Instructions
|
|
</Label>
|
|
<Textarea
|
|
id="system-instructions"
|
|
value={formData.system_prompt}
|
|
onChange={(e) => handleInputChange('system_prompt', e.target.value)}
|
|
placeholder="Describe the agent's role, behavior, and expertise..."
|
|
className="min-h-[250px] resize-none"
|
|
disabled={createAgentMutation.isPending}
|
|
onFocus={(e) => {
|
|
if (e.target.value === "Describe the agent's role, behavior, and expertise...") {
|
|
handleInputChange('system_prompt', '');
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-l w-[60%] bg-muted/30 flex flex-col min-h-0">
|
|
<Tabs defaultValue="tools" className="flex flex-col h-full">
|
|
<TabsList className="w-full justify-start rounded-none border-b h-10">
|
|
<TabsTrigger
|
|
value="tools"
|
|
>
|
|
<Settings2 className="h-4 w-4" />
|
|
Default Tools
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="mcp"
|
|
>
|
|
<Sparkles className="h-4 w-4" />
|
|
MCP Servers
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="tools" className="flex-1 flex flex-col m-0 min-h-0">
|
|
<div className="px-6 py-4 border-b bg-background flex-shrink-0">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold">Available Tools</h3>
|
|
<span className="text-sm text-muted-foreground">
|
|
{getSelectedToolsCount()} selected
|
|
</span>
|
|
</div>
|
|
|
|
<div className="relative mb-4">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="Search tools..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="pl-10 h-10"
|
|
disabled={createAgentMutation.isPending}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-2 flex-wrap">
|
|
{TOOL_CATEGORIES.map((category) => (
|
|
<Button
|
|
key={category}
|
|
variant={selectedCategory === category ? 'default' : 'outline'}
|
|
size="sm"
|
|
onClick={() => setSelectedCategory(category)}
|
|
disabled={createAgentMutation.isPending}
|
|
className="px-3 py-1.5 h-auto text-xs font-medium rounded-full"
|
|
>
|
|
{category}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent p-6 min-h-0">
|
|
<div className="space-y-3">
|
|
{getFilteredTools().map(([toolName, toolInfo]) => (
|
|
<div
|
|
key={toolName}
|
|
className="flex items-center gap-3 p-3 bg-card rounded-lg border hover:border-border/80 transition-colors"
|
|
>
|
|
<div className={`w-10 h-10 rounded-lg ${toolInfo.color} flex items-center justify-center flex-shrink-0`}>
|
|
<span className="text-lg">{toolInfo.icon}</span>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center justify-between mb-1">
|
|
<h4 className="font-medium text-sm">
|
|
{getToolDisplayName(toolName)}
|
|
</h4>
|
|
<Switch
|
|
checked={formData.agentpress_tools?.[toolName]?.enabled || false}
|
|
onCheckedChange={(checked) => handleToolToggle(toolName, checked)}
|
|
disabled={createAgentMutation.isPending}
|
|
className="flex-shrink-0"
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
{toolInfo.description}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{getFilteredTools().length === 0 && (
|
|
<div className="text-center py-8">
|
|
<div className="text-4xl mb-3">🔍</div>
|
|
<h3 className="text-sm font-medium mb-1">No tools found</h3>
|
|
<p className="text-xs text-muted-foreground">Try adjusting your search criteria</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="mcp" className="flex-1 m-0 p-6 overflow-y-auto">
|
|
<MCPConfigurationNew
|
|
configuredMCPs={formData.configured_mcps.map(mcp => ({
|
|
...mcp,
|
|
enabledTools: mcp.enabledTools || []
|
|
}))}
|
|
onConfigurationChange={handleMCPConfigurationChange}
|
|
/>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="px-6 border-t py-4 flex-shrink-0">
|
|
<div className="flex justify-end gap-3">
|
|
<Button
|
|
variant="outline"
|
|
onClick={handleCancel}
|
|
disabled={createAgentMutation.isPending || isSubmitting}
|
|
className="px-6"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={handleSubmit}
|
|
disabled={createAgentMutation.isPending || isSubmitting || !formData.name.trim()}
|
|
>
|
|
{createAgentMutation.isPending || isSubmitting ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
Creating Agent
|
|
</>
|
|
) : (
|
|
'Create Agent'
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
} |