This commit is contained in:
marko-kraemer 2025-07-10 05:16:42 +02:00
parent ef3101fbb7
commit 37c781a99b
5 changed files with 443 additions and 871 deletions

View File

@ -23,7 +23,6 @@ import { usePipedreamProfiles } from '@/hooks/react-query/pipedream/use-pipedrea
import { useFeatureFlag } from '@/lib/feature-flags';
// Import components from existing pages
import { UpdateAgentDialog } from '../../../components/agents/update-agent-dialog';
import { SearchAndFilters } from '../../../components/agents/search-and-filters';
import { ResultsInfo } from '../../../components/agents/results-info';
import { EmptyState } from '../../../components/agents/empty-state';
@ -1508,24 +1507,6 @@ export default function AgentsPage() {
className="pl-10"
/>
</div>
{allTags.length > 0 && (
<div className="space-y-2">
<p className="text-sm font-medium text-muted-foreground">Filter by tags:</p>
<div className="flex flex-wrap gap-2">
{allTags.map(tag => (
<Badge
key={tag}
variant={marketplaceSelectedTags.includes(tag) ? "default" : "outline"}
className="cursor-pointer hover:bg-primary/80"
onClick={() => handleTagFilter(tag)}
>
<Tags className="h-3 w-3 mr-1" />
{tag}
</Badge>
))}
</div>
</div>
)}
</div>
<div className="text-sm text-muted-foreground">
@ -1941,17 +1922,6 @@ export default function AgentsPage() {
</TabsContent>
</Tabs>
{/* Dialogs */}
<UpdateAgentDialog
agentId={editingAgentId}
isOpen={editDialogOpen}
onOpenChange={(open) => {
setEditDialogOpen(open);
if (!open) setEditingAgentId(null);
}}
onAgentUpdated={loadAgents}
/>
{/* Publish Dialog */}
<Dialog open={!!publishDialog} onOpenChange={() => setPublishDialog(null)}>
<DialogContent className="sm:max-w-md">

View File

@ -0,0 +1,357 @@
'use client';
import React, { useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button';
import { X, Settings2, Brain, Database, Zap, Workflow, Bot } from 'lucide-react';
import { AgentMCPConfiguration } from './agent-mcp-configuration';
import { AgentTriggersConfiguration } from './triggers/agent-triggers-configuration';
import { AgentWorkflowsConfiguration } from './workflows/agent-workflows-configuration';
import { AgentKnowledgeBaseManager } from './knowledge-base/agent-knowledge-base-manager';
import { AgentToolsConfiguration } from './agent-tools-configuration';
import { useAgent, useUpdateAgent } from '@/hooks/react-query/agents/use-agents';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { toast } from 'sonner';
import { useRouter } from 'next/navigation';
interface AgentConfigModalProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
selectedAgentId?: string;
onAgentSelect?: (agentId: string | undefined) => void;
initialTab?: string;
}
export const AgentConfigModal: React.FC<AgentConfigModalProps> = ({
isOpen,
onOpenChange,
selectedAgentId,
onAgentSelect,
initialTab = 'integrations'
}) => {
const [activeTab, setActiveTab] = useState(initialTab);
const [editingInstructions, setEditingInstructions] = useState(false);
const [instructionsValue, setInstructionsValue] = useState('');
const [agentName, setAgentName] = useState('');
const [agentDescription, setAgentDescription] = useState('');
const { data: agent, isLoading } = useAgent(selectedAgentId || '');
const updateAgentMutation = useUpdateAgent();
const router = useRouter();
const handleAgentSelect = (agentId: string | undefined) => {
onAgentSelect?.(agentId);
};
// Update local state when agent data changes
React.useEffect(() => {
if (agent) {
setAgentName(agent.name || '');
setAgentDescription(agent.description || '');
setInstructionsValue(agent.system_prompt || '');
}
}, [agent]);
const handleSaveInstructions = async () => {
if (!selectedAgentId) return;
try {
await updateAgentMutation.mutateAsync({
agentId: selectedAgentId,
name: agentName,
description: agentDescription,
system_prompt: instructionsValue
});
toast.success('Agent updated successfully');
setEditingInstructions(false);
} catch (error) {
toast.error('Failed to update agent');
}
};
const handleToolsChange = async (tools: Record<string, { enabled: boolean; description: string }>) => {
if (!selectedAgentId) return;
try {
await updateAgentMutation.mutateAsync({
agentId: selectedAgentId,
agentpress_tools: tools
});
toast.success('Tools updated successfully');
} catch (error) {
toast.error('Failed to update tools');
}
};
const handleMCPChange = async (mcps: any) => {
if (!selectedAgentId) return;
try {
await updateAgentMutation.mutateAsync({
agentId: selectedAgentId,
configured_mcps: mcps.configured_mcps || [],
custom_mcps: mcps.custom_mcps || []
});
toast.success('Integrations updated successfully');
} catch (error) {
toast.error('Failed to update integrations');
}
};
const displayName = agent?.name || 'Suna';
return (
<>
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-5xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader className="flex-shrink-0 pb-2">
<div className="flex items-center justify-between">
<DialogTitle className="flex items-center gap-2">
<Settings2 className="h-5 w-5" />
Agent Configuration
</DialogTitle>
<Button
variant="ghost"
size="sm"
onClick={() => onOpenChange(false)}
>
<X className="h-4 w-4" />
</Button>
</div>
</DialogHeader>
<div className="flex-1 overflow-hidden flex flex-col">
{/* Agent Selector */}
<div className="flex-shrink-0 border-b pb-4 mb-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{/* <AgentSelector
selectedAgentId={selectedAgentId}
onAgentSelect={handleAgentSelect}
className="min-w-[250px]"
/> */}
</div>
{selectedAgentId && (
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => router.push(`/agents/config/${selectedAgentId}`)}
className="flex items-center gap-2"
>
<Settings2 className="h-4 w-4" />
Edit Agent
</Button>
</div>
)}
</div>
</div>
{/* Configuration Tabs */}
<div className="flex-1 overflow-hidden">
<Tabs value={activeTab} onValueChange={setActiveTab} className="h-full flex flex-col">
<TabsList className="grid w-full grid-cols-6 flex-shrink-0">
<TabsTrigger value="integrations" className="flex items-center gap-2">
<span className="hidden sm:inline">Integrations</span>
</TabsTrigger>
<TabsTrigger value="tools" className="flex items-center gap-2">
<Settings2 className="h-4 w-4" />
<span className="hidden sm:inline">Tools</span>
</TabsTrigger>
<TabsTrigger value="instructions" className="flex items-center gap-2">
<Brain className="h-4 w-4" />
<span className="hidden sm:inline">Instructions</span>
</TabsTrigger>
<TabsTrigger value="knowledge" className="flex items-center gap-2">
<Database className="h-4 w-4" />
<span className="hidden sm:inline">Knowledge Base</span>
</TabsTrigger>
<TabsTrigger value="triggers" className="flex items-center gap-2">
<Zap className="h-4 w-4" />
<span className="hidden sm:inline">Triggers</span>
</TabsTrigger>
<TabsTrigger value="workflows" className="flex items-center gap-2">
<Workflow className="h-4 w-4" />
<span className="hidden sm:inline">Workflows</span>
</TabsTrigger>
</TabsList>
<div className="flex-1 overflow-hidden mt-4">
<TabsContent value="integrations" className="h-full m-0 overflow-y-auto">
<div className="space-y-4">
<div className="text-center py-8">
<h3 className="text-lg font-semibold mb-2">MCP Integrations</h3>
<p className="text-muted-foreground mb-4">
Connect your agent to external services and tools
</p>
{selectedAgentId && (
<AgentMCPConfiguration
configuredMCPs={agent?.configured_mcps || []}
customMCPs={agent?.custom_mcps || []}
onMCPChange={handleMCPChange}
/>
)}
</div>
</div>
</TabsContent>
<TabsContent value="tools" className="h-full m-0 overflow-y-auto">
<div className="space-y-4 p-4">
{selectedAgentId ? (
<div className="max-w-4xl">
<div className="mb-4">
<h3 className="text-lg font-semibold mb-2">Standard Tools</h3>
<p className="text-muted-foreground mb-4">
Configure the built-in tools available to your agent
</p>
</div>
<AgentToolsConfiguration
tools={agent?.agentpress_tools || {}}
onToolsChange={handleToolsChange}
/>
</div>
) : (
<div className="text-center py-8">
<Settings2 className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<h3 className="text-lg font-semibold mb-2">Standard Tools</h3>
<p className="text-muted-foreground mb-4">
Select an agent to configure its tools
</p>
</div>
)}
</div>
</TabsContent>
<TabsContent value="instructions" className="h-full m-0 overflow-y-auto">
<div className="space-y-4 p-4">
{selectedAgentId ? (
<div className="max-w-2xl">
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="agent-name">Agent Name</Label>
<Input
id="agent-name"
value={agentName}
onChange={(e) => setAgentName(e.target.value)}
placeholder="Enter agent name"
/>
</div>
<div className="space-y-2">
<Label htmlFor="agent-description">Description</Label>
<Input
id="agent-description"
value={agentDescription}
onChange={(e) => setAgentDescription(e.target.value)}
placeholder="Brief description of the agent"
/>
</div>
<div className="space-y-2">
<Label htmlFor="system-instructions">System Instructions</Label>
<Textarea
id="system-instructions"
value={instructionsValue}
onChange={(e) => setInstructionsValue(e.target.value)}
placeholder="Describe the agent's role, behavior, and expertise..."
className="min-h-[200px]"
/>
</div>
<div className="flex gap-2">
<Button
onClick={handleSaveInstructions}
disabled={updateAgentMutation.isPending}
>
{updateAgentMutation.isPending ? 'Saving...' : 'Save Changes'}
</Button>
<Button
variant="outline"
onClick={() => {
setAgentName(agent?.name || '');
setAgentDescription(agent?.description || '');
setInstructionsValue(agent?.system_prompt || '');
}}
>
Reset
</Button>
</div>
</div>
</div>
) : (
<div className="text-center py-8">
<Brain className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<h3 className="text-lg font-semibold mb-2">System Instructions</h3>
<p className="text-muted-foreground mb-4">
Select an agent to configure its instructions
</p>
</div>
)}
</div>
</TabsContent>
<TabsContent value="knowledge" className="h-full m-0 overflow-y-auto">
<div className="space-y-4">
{selectedAgentId ? (
<AgentKnowledgeBaseManager
agentId={selectedAgentId}
agentName={agentName}
/>
) : (
<div className="text-center py-8">
<Database className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<h3 className="text-lg font-semibold mb-2">Knowledge Base</h3>
<p className="text-muted-foreground mb-4">
Select an agent to manage its knowledge base
</p>
</div>
)}
</div>
</TabsContent>
<TabsContent value="triggers" className="h-full m-0 overflow-y-auto">
<div className="space-y-4">
{selectedAgentId ? (
<AgentTriggersConfiguration agentId={selectedAgentId} />
) : (
<div className="text-center py-8">
<Zap className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<h3 className="text-lg font-semibold mb-2">Triggers</h3>
<p className="text-muted-foreground mb-4">
Select an agent to configure its triggers
</p>
</div>
)}
</div>
</TabsContent>
<TabsContent value="workflows" className="h-full m-0 overflow-y-auto">
<div className="space-y-4">
{selectedAgentId ? (
<AgentWorkflowsConfiguration
agentId={selectedAgentId}
agentName={agentName}
/>
) : (
<div className="text-center py-8">
<Workflow className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<h3 className="text-lg font-semibold mb-2">Workflows</h3>
<p className="text-muted-foreground mb-4">
Select an agent to configure its workflows
</p>
</div>
)}
</div>
</TabsContent>
</div>
</Tabs>
</div>
</div>
</DialogContent>
</Dialog>
</>
);
};

View File

@ -1,472 +0,0 @@
import React, { useState, useEffect } 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, Save, Settings2, Sparkles, GitBranch } from 'lucide-react';
import { toast } from 'sonner';
import { Skeleton } from '@/components/ui/skeleton';
import { DEFAULT_AGENTPRESS_TOOLS, getToolDisplayName } from './tools';
import { useAgent, useUpdateAgent } from '@/hooks/react-query/agents/use-agents';
import { MCPConfigurationNew } from './mcp/mcp-configuration-new';
import { AgentVersionManager } from './AgentVersionManager';
import { Badge } from '@/components/ui/badge';
interface AgentUpdateRequest {
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 UpdateAgentDialogProps {
agentId: string | null;
isOpen: boolean;
onOpenChange: (open: boolean) => void;
onAgentUpdated?: () => void;
}
const TOOL_CATEGORIES = ['All', 'AI', 'Code', 'Integration', 'Search', 'File', 'Data'];
export const UpdateAgentDialog = ({ agentId, isOpen, onOpenChange, onAgentUpdated }: UpdateAgentDialogProps) => {
const [searchQuery, setSearchQuery] = useState<string>('');
const [selectedCategory, setSelectedCategory] = useState<string>('All');
const [formData, setFormData] = useState<AgentUpdateRequest>({});
const {
data: agent,
isLoading,
error
} = useAgent(agentId || '');
const updateAgentMutation = useUpdateAgent();
useEffect(() => {
if (!isOpen) {
setSearchQuery('');
setSelectedCategory('All');
setFormData({});
}
}, [isOpen]);
useEffect(() => {
if (agent && isOpen) {
setFormData({
name: agent.name,
description: agent.description || '',
system_prompt: agent.system_prompt,
configured_mcps: (agent.configured_mcps || []).map(mcp => ({
name: mcp.name,
qualifiedName: (mcp as any).qualifiedName || mcp.name,
config: mcp.config,
enabledTools: (mcp as any).enabledTools || []
})),
custom_mcps: agent.custom_mcps || [],
agentpress_tools: agent.agentpress_tools || {},
is_default: agent.is_default,
});
}
}, [agent, isOpen]);
useEffect(() => {
if (error && isOpen) {
console.error('Error loading agent:', error);
toast.error(error instanceof Error ? error.message : 'Failed to load agent');
onOpenChange(false);
}
}, [error, isOpen, onOpenChange]);
const handleInputChange = (field: keyof AgentUpdateRequest, 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 getAllAgentPressTools = () => {
const existing = formData.agentpress_tools || {};
const merged = { ...DEFAULT_AGENTPRESS_TOOLS };
Object.keys(existing).forEach(key => {
merged[key] = { ...merged[key], ...existing[key] };
});
return merged;
};
const getSelectedToolsCount = (): number => {
const tools = getAllAgentPressTools();
return Object.values(tools).filter(tool => tool.enabled).length;
};
const getFilteredTools = (): Array<[string, any]> => {
let tools = Object.entries(getAllAgentPressTools());
if (searchQuery) {
tools = tools.filter(([toolName, toolInfo]) =>
getToolDisplayName(toolName).toLowerCase().includes(searchQuery.toLowerCase()) ||
toolInfo.description.toLowerCase().includes(searchQuery.toLowerCase())
);
}
return tools;
};
const handleSubmit = async () => {
if (!formData.name?.trim()) {
toast.error('Agent name is required');
return;
}
if (!formData.system_prompt?.trim()) {
toast.error('System prompt is required');
return;
}
if (!agentId) {
toast.error('Invalid agent ID');
return;
}
try {
await updateAgentMutation.mutateAsync({
agentId,
...formData
});
toast.success('Agent updated successfully!');
onOpenChange(false);
onAgentUpdated?.();
} catch (error: any) {
console.error('Error updating agent:', error);
if (error.message?.includes('System prompt cannot be empty')) {
toast.error('System prompt cannot be empty');
} else if (error.message?.includes('Failed to create new agent version')) {
toast.error('Failed to create new version. Please try again.');
} else if (error.message?.includes('Failed to update agent')) {
toast.error('Failed to update agent. Please check your configuration and try again.');
} else if (error.message?.includes('Agent not found')) {
toast.error('Agent not found. It may have been deleted.');
onOpenChange(false);
} else if (error.message?.includes('Access denied')) {
toast.error('You do not have permission to update this agent.');
onOpenChange(false);
} else {
toast.error(error.message || 'Failed to update agent. Please try again.');
}
}
};
const handleCancel = () => {
onOpenChange(false);
};
if (isLoading || !agent) {
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">
<DialogTitle className="text-xl font-semibold">
Edit Agent
</DialogTitle>
<DialogDescription className="text-sm mt-1">
Loading agent configuration...
</DialogDescription>
</DialogHeader>
<div className="flex-1 p-6">
<div className="space-y-4">
{[1, 2, 3].map(i => (
<div key={i} className="space-y-2">
<Skeleton className="h-6 w-48" />
<Skeleton className="h-32 w-full" />
</div>
))}
</div>
</div>
</DialogContent>
</Dialog>
);
}
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 flex items-center gap-2">
Edit Agent
{(agent as any).current_version && (
<Badge variant="secondary" className="text-xs">
{(agent as any).current_version.version_name}
</Badge>
)}
</DialogTitle>
<DialogDescription className="text-sm mt-1">
Modify your agent's configuration and capabilities
</DialogDescription>
</div>
</div>
</DialogHeader>
<div className="flex-1 w-full overflow-hidden min-h-0">
<div className="flex w-full h-full">
{/* Left Panel - Basic Configuration */}
<div className="p-6 py-4 w-[40%] space-y-6 overflow-y-auto scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent">
<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={updateAgentMutation.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={updateAgentMutation.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={updateAgentMutation.isPending}
/>
</div>
</div>
{/* Right Panel - Tools & MCP */}
<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>
<TabsTrigger
value="versions"
>
<GitBranch className="h-4 w-4" />
Versions
</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={updateAgentMutation.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={updateAgentMutation.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={toolInfo.enabled || false}
onCheckedChange={(checked) => handleToolToggle(toolName, checked)}
disabled={updateAgentMutation.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 || []
})),
...(formData.custom_mcps || []).map(customMcp => ({
name: customMcp.name,
qualifiedName: `custom_${customMcp.type}_${customMcp.name.replace(' ', '_').toLowerCase()}`,
config: customMcp.config,
enabledTools: customMcp.enabledTools,
isCustom: true,
customType: customMcp.type
}))
]}
onConfigurationChange={handleMCPConfigurationChange}
/>
</TabsContent>
<TabsContent value="versions" className="flex-1 m-0 p-6 overflow-y-auto">
<AgentVersionManager
agent={agent as any}
onCreateVersion={() => {
// When creating a new version, save current changes first
handleSubmit();
}}
/>
</TabsContent>
</Tabs>
</div>
</div>
</div>
<div className="px-6 border-t py-4 flex-shrink-0">
<div className="space-y-3">
{/* Show notice if changes will create a new version */}
{agent && (formData.system_prompt !== agent.system_prompt ||
JSON.stringify(formData.configured_mcps) !== JSON.stringify(agent.configured_mcps) ||
JSON.stringify(formData.custom_mcps) !== JSON.stringify(agent.custom_mcps) ||
JSON.stringify(formData.agentpress_tools) !== JSON.stringify(agent.agentpress_tools)) && (
<div className="flex items-center gap-2 text-sm text-muted-foreground bg-muted/50 px-3 py-2 rounded-md">
<GitBranch className="h-4 w-4" />
<span>These changes will create a new version of your agent</span>
</div>
)}
<div className="flex justify-end gap-3">
<Button
variant="outline"
onClick={handleCancel}
disabled={updateAgentMutation.isPending}
className="px-6"
>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={updateAgentMutation.isPending || !formData.name?.trim() || !formData.system_prompt?.trim()}
>
{updateAgentMutation.isPending ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Saving Changes
</>
) : (
<>
<Save className="h-4 w-4" />
Save Changes
</>
)}
</Button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@ -1,329 +0,0 @@
'use client';
import React, { useState } from 'react';
import { ChevronDown, Plus, Star, Bot, Edit, User } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Badge } from '@/components/ui/badge';
import { useAgents } from '@/hooks/react-query/agents/use-agents';
import { useRouter } from 'next/navigation';
import { cn } from '@/lib/utils';
import { CreateAgentDialog } from '@/components/agents/create-agent-dialog';
import { useFeatureFlags } from '@/lib/feature-flags';
interface AgentSelectorProps {
onAgentSelect?: (agentId: string | undefined) => void;
selectedAgentId?: string;
className?: string;
variant?: 'default' | 'heading';
}
export function AgentSelector({
onAgentSelect,
selectedAgentId,
className,
variant = 'default',
}: AgentSelectorProps) {
const { data: agentsResponse, isLoading, refetch: loadAgents } = useAgents({
limit: 100,
sort_by: 'name',
sort_order: 'asc'
});
const { flags, loading: flagsLoading } = useFeatureFlags(['custom_agents']);
const customAgentsEnabled = flags.custom_agents;
const router = useRouter();
const [isOpen, setIsOpen] = useState(false);
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const agents = agentsResponse?.agents || [];
const defaultAgent = agents.find(agent => agent.is_default);
const currentAgent = selectedAgentId
? agents.find(agent => agent.agent_id === selectedAgentId)
: null;
const displayName = currentAgent?.name || defaultAgent?.name || 'Suna';
const agentAvatar = currentAgent?.avatar;
const isUsingSuna = !currentAgent && !defaultAgent;
const handleAgentSelect = (agentId: string | undefined) => {
onAgentSelect?.(agentId);
setIsOpen(false);
};
const handleCreateAgent = () => {
setCreateDialogOpen(true);
setIsOpen(false);
};
const handleManageAgents = () => {
router.push('/agents');
setIsOpen(false);
};
const handleClearSelection = () => {
onAgentSelect?.(undefined);
setIsOpen(false);
};
if (!customAgentsEnabled) {
if (variant === 'heading') {
return (
<div className={cn("flex items-center", className)}>
<span className="tracking-tight text-4xl font-semibold leading-tight text-primary">
Suna
</span>
</div>
);
}
}
if (isLoading) {
if (variant === 'heading') {
return (
<div className={cn("flex items-center", className)}>
<span className="tracking-tight text-4xl font-semibold leading-tight text-muted-foreground">
Loading...
</span>
</div>
);
}
return (
<div className={cn("flex items-center gap-2", className)}>
<div className="flex items-center gap-2 px-3 py-2 rounded-lg border bg-background">
<Bot className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">Loading agents...</span>
</div>
</div>
);
}
if (variant === 'heading') {
return (
<>
<div className={cn("flex items-center", className)}>
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="flex items-center gap-1 px-2 py-1 h-auto hover:bg-transparent hover:text-primary transition-colors group"
>
<span className="underline decoration-dashed underline-offset-6 decoration-muted-foreground/50 tracking-tight text-4xl font-semibold leading-tight text-primary">
{displayName}
<span className="text-muted-foreground ml-2">
{agentAvatar && agentAvatar}
</span>
</span>
<div className="flex items-center opacity-60 group-hover:opacity-100 transition-opacity">
<ChevronDown className="h-5 w-5 text-muted-foreground" />
<Edit className="h-4 w-4 text-muted-foreground ml-1" />
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[320px]">
<div className="px-3 py-2">
<p className="text-sm font-medium">Select an agent</p>
<p className="text-xs text-muted-foreground">You can create your own agent</p>
</div>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => handleClearSelection()}
className="flex flex-col items-start gap-1 p-3 cursor-pointer"
>
<div className="flex items-center gap-2 w-full">
<User className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<div className="flex items-center gap-1 flex-1 min-w-0">
<span className="font-medium truncate">Suna</span>
<Badge variant="outline" className="text-xs px-1 py-0 flex-shrink-0">
Default
</Badge>
</div>
{isUsingSuna && (
<div className="h-2 w-2 rounded-full bg-primary flex-shrink-0" />
)}
</div>
<span className="text-xs text-muted-foreground pl-6 line-clamp-2">
Your personal AI employee
</span>
</DropdownMenuItem>
{agents.length > 0 ? (
<>
{agents.map((agent) => (
<DropdownMenuItem
key={agent.agent_id}
onClick={() => handleAgentSelect(agent.agent_id)}
className="flex flex-col items-start gap-1 p-3 cursor-pointer"
>
<div className="flex items-center gap-2 w-full">
{agent.avatar}
<div className="flex items-center gap-1 flex-1 min-w-0">
<span className="font-medium truncate">{agent.name}</span>
{agent.is_default && (
<Badge variant="secondary" className="text-xs px-1 py-0 flex-shrink-0">
<Star className="h-2.5 w-2.5 mr-0.5 fill-current" />
System
</Badge>
)}
</div>
{currentAgent?.agent_id === agent.agent_id && (
<div className="h-2 w-2 rounded-full bg-primary flex-shrink-0" />
)}
</div>
{agent.description && (
<span className="text-xs text-muted-foreground pl-6 line-clamp-2">
{agent.description}
</span>
)}
</DropdownMenuItem>
))}
</>
) : null}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleCreateAgent} className="cursor-pointer">
Agents
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</>
);
}
return (
<>
<div className={cn("flex items-center gap-2", className)}>
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="flex items-center gap-2 px-3 py-2 h-auto min-w-[200px] justify-between"
>
<div className="flex items-center gap-2">
{isUsingSuna ? (
<User className="h-4 w-4 text-muted-foreground" />
) : (
<Bot className="h-4 w-4 text-muted-foreground" />
)}
<div className="flex flex-col items-start">
<div className="flex items-center gap-1">
<span className="text-sm font-medium">
{displayName}
</span>
{isUsingSuna && (
<Badge variant="outline" className="text-xs px-1 py-0">
Default
</Badge>
)}
{currentAgent?.is_default && (
<Badge variant="secondary" className="text-xs px-1 py-0">
<Star className="h-2.5 w-2.5 mr-0.5 fill-current" />
System
</Badge>
)}
</div>
{currentAgent?.description ? (
<span className="text-xs text-muted-foreground line-clamp-1 max-w-[150px]">
{currentAgent.description}
</span>
) : isUsingSuna ? (
<span className="text-xs text-muted-foreground line-clamp-1 max-w-[150px]">
Your personal AI employee
</span>
) : null}
</div>
</div>
<ChevronDown className="h-4 w-4 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[280px]">
<DropdownMenuItem
onClick={() => handleClearSelection()}
className="flex flex-col items-start gap-1 p-3 cursor-pointer"
>
<div className="flex items-center gap-2 w-full">
<User className="h-4 w-4 text-muted-foreground" />
<div className="flex items-center gap-1 flex-1">
<span className="font-medium">Suna</span>
<Badge variant="outline" className="text-xs px-1 py-0">
Default
</Badge>
</div>
{isUsingSuna && (
<div className="h-2 w-2 rounded-full bg-primary" />
)}
</div>
<span className="text-xs text-muted-foreground pl-6 line-clamp-2">
Your personal AI employee
</span>
</DropdownMenuItem>
{agents.length > 0 ? (
<>
{agents.map((agent) => (
<DropdownMenuItem
key={agent.agent_id}
onClick={() => handleAgentSelect(agent.agent_id)}
className="flex flex-col items-start gap-1 p-3 cursor-pointer"
>
<div className="flex items-center gap-2 w-full">
<Bot className="h-4 w-4 text-muted-foreground" />
<div className="flex items-center gap-1 flex-1">
<span className="font-medium">{agent.name}</span>
{agent.is_default && (
<Badge variant="secondary" className="text-xs px-1 py-0">
<Star className="h-2.5 w-2.5 mr-0.5 fill-current" />
System
</Badge>
)}
</div>
{currentAgent?.agent_id === agent.agent_id && (
<div className="h-2 w-2 rounded-full bg-primary" />
)}
</div>
{agent.description && (
<span className="text-xs text-muted-foreground pl-6 line-clamp-2">
{agent.description}
</span>
)}
</DropdownMenuItem>
))}
</>
) : null}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleCreateAgent} className="cursor-pointer">
<Plus className="h-4 w-4" />
Create New Agent
</DropdownMenuItem>
<DropdownMenuItem onClick={handleManageAgents} className="cursor-pointer">
<Bot className="h-4 w-4" />
Manage All Agents
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<CreateAgentDialog
isOpen={createDialogOpen}
onOpenChange={setCreateDialogOpen}
onAgentCreated={loadAgents}
/>
</>
);
}

View File

@ -15,10 +15,10 @@ import { useModelSelection } from './_use-model-selection';
import { useFileDelete } from '@/hooks/react-query/files';
import { useQueryClient } from '@tanstack/react-query';
import { FloatingToolPreview, ToolCallInput } from './floating-tool-preview';
import { Settings2, Sparkles, Brain, ChevronRight, Zap, Workflow } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Settings2, Sparkles, Brain, ChevronRight, Zap, Workflow, Database, Wrench } from 'lucide-react';
import { FaGoogle, FaDiscord } from 'react-icons/fa';
import { SiNotion } from 'react-icons/si';
import { AgentConfigModal } from '@/components/agents/agent-config-modal';
export interface ChatInputHandles {
getPendingFiles: () => File[];
@ -105,6 +105,8 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
const [pendingFiles, setPendingFiles] = useState<File[]>([]);
const [isUploading, setIsUploading] = useState(false);
const [isDraggingOver, setIsDraggingOver] = useState(false);
const [configModalOpen, setConfigModalOpen] = useState(false);
const [configModalTab, setConfigModalTab] = useState('integrations');
const {
selectedModel,
@ -322,47 +324,82 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
</CardContent>
{enableAdvancedConfig && selectedAgentId && (
<div
className="w-full border-t border-border/30 bg-muted/20 px-4 py-2.5 rounded-b-3xl border-l border-r border-b border-border cursor-pointer hover:bg-muted/30 transition-colors"
onClick={() => onConfigureAgent?.(selectedAgentId)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 min-w-0 flex-1">
<div className="flex items-center gap-2 text-xs min-w-0 flex-1">
<div className="flex items-center gap-1 text-muted-foreground">
<span className="hidden sm:inline">Integrations</span>
<span className="sm:hidden">Integrations</span>
<div className="flex items-center -space-x-1 ml-1">
<div className="w-5 h-5 bg-white border border-border rounded-full flex items-center justify-center shadow-sm">
<FaGoogle className="w-2.5 h-2.5" />
</div>
<div className="w-5 h-5 bg-white border border-border rounded-full flex items-center justify-center shadow-sm">
<FaDiscord className="w-2.5 h-2.5" />
</div>
<div className="w-5 h-5 bg-white border border-border rounded-full flex items-center justify-center shadow-sm">
<SiNotion className="w-2.5 h-2.5" />
</div>
<div className="w-full border-t border-border/30 bg-muted/20 px-4 py-2.5 rounded-b-3xl border-l border-r border-b border-border">
<div className="flex items-center justify-center">
<div className="flex items-center gap-1 sm:gap-2 overflow-x-auto scrollbar-none">
<button
onClick={() => {
setConfigModalTab('integrations');
setConfigModalOpen(true);
}}
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-all duration-200 px-2.5 py-1.5 rounded-md hover:bg-muted/50 border border-transparent hover:border-border/30 flex-shrink-0"
>
<div className="flex items-center -space-x-0.5">
<div className="w-4 h-4 bg-white border border-border rounded-full flex items-center justify-center shadow-sm">
<FaGoogle className="w-2 h-2" />
</div>
<div className="w-4 h-4 bg-white border border-border rounded-full flex items-center justify-center shadow-sm">
<FaDiscord className="w-2 h-2" />
</div>
<div className="w-4 h-4 bg-white border border-border rounded-full flex items-center justify-center shadow-sm">
<SiNotion className="w-2 h-2" />
</div>
</div>
<div className="w-1 h-1 bg-muted-foreground/60 rounded-full hidden sm:block" />
<div className="flex items-center gap-1 text-muted-foreground">
<Brain className="h-2.5 w-2.5 flex-shrink-0" />
<span className="hidden sm:inline">Instructions</span>
<span className="sm:hidden">Instructions</span>
</div>
<div className="w-1 h-1 bg-muted-foreground/60 rounded-full hidden sm:block" />
<div className="flex items-center gap-1 text-muted-foreground hidden sm:flex">
<Zap className="h-2.5 w-2.5 flex-shrink-0" />
<span>Triggers</span>
</div>
<div className="w-1 h-1 bg-muted-foreground/60 rounded-full hidden sm:block" />
<div className="flex items-center gap-1 text-muted-foreground hidden sm:flex">
<Workflow className="h-2.5 w-2.5 flex-shrink-0" />
<span>Workflows</span>
</div>
</div>
<span className="text-xs font-medium">Integrations</span>
</button>
<div className="w-px h-4 bg-border/60" />
<button
onClick={() => {
setConfigModalTab('instructions');
setConfigModalOpen(true);
}}
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-all duration-200 px-2.5 py-1.5 rounded-md hover:bg-muted/50 border border-transparent hover:border-border/30 flex-shrink-0"
>
<Brain className="h-3.5 w-3.5 flex-shrink-0" />
<span className="text-xs font-medium">Instructions</span>
</button>
<div className="w-px h-4 bg-border/60" />
<button
onClick={() => {
setConfigModalTab('knowledge');
setConfigModalOpen(true);
}}
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-all duration-200 px-2.5 py-1.5 rounded-md hover:bg-muted/50 border border-transparent hover:border-border/30 flex-shrink-0"
>
<Database className="h-3.5 w-3.5 flex-shrink-0" />
<span className="text-xs font-medium">Knowledge</span>
</button>
<div className="w-px h-4 bg-border/60" />
<button
onClick={() => {
setConfigModalTab('triggers');
setConfigModalOpen(true);
}}
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-all duration-200 px-2.5 py-1.5 rounded-md hover:bg-muted/50 border border-transparent hover:border-border/30 flex-shrink-0"
>
<Zap className="h-3.5 w-3.5 flex-shrink-0" />
<span className="text-xs font-medium">Triggers</span>
</button>
<div className="w-px h-4 bg-border/60" />
<button
onClick={() => {
setConfigModalTab('workflows');
setConfigModalOpen(true);
}}
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-all duration-200 px-2.5 py-1.5 rounded-md hover:bg-muted/50 border border-transparent hover:border-border/30 flex-shrink-0"
>
<Workflow className="h-3.5 w-3.5 flex-shrink-0" />
<span className="text-xs font-medium">Workflows</span>
</button>
</div>
<ChevronRight className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
</div>
</div>
)}
@ -382,6 +419,15 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
</motion.div>
)} */}
{/* Agent Configuration Modal */}
<AgentConfigModal
isOpen={configModalOpen}
onOpenChange={setConfigModalOpen}
selectedAgentId={selectedAgentId}
onAgentSelect={onAgentSelect}
initialTab={configModalTab}
/>
</div>
);
},