mirror of https://github.com/kortix-ai/suna.git
feat: improve agent config modal UX with seamless agent switching
- Remove 'General' tab from agent configuration modal - Add inline agent switcher integrated into dialog title area - Fix name editing functionality to work with agent switching - Display proper Suna icons in agent dropdown - Remove redundant description text for cleaner UI - Fix DialogTitle accessibility requirements - Clean up duplicate close buttons The agent switcher now seamlessly replaces the static title when onAgentChange is provided, creating a smooth dropdown experience that maintains visual hierarchy while enabling easy agent switching without closing the modal.
This commit is contained in:
parent
bc6620569f
commit
2c2eaf61f8
|
@ -8,9 +8,14 @@ import {
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsContent,
|
TabsContent,
|
||||||
|
@ -35,13 +40,15 @@ import {
|
||||||
Edit3,
|
Edit3,
|
||||||
Save,
|
Save,
|
||||||
Brain,
|
Brain,
|
||||||
|
ChevronDown,
|
||||||
|
Search,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { KortixLogo } from '@/components/sidebar/kortix-logo';
|
import { KortixLogo } from '@/components/sidebar/kortix-logo';
|
||||||
|
|
||||||
import { useAgentVersionData } from '@/hooks/use-agent-version-data';
|
import { useAgentVersionData } from '@/hooks/use-agent-version-data';
|
||||||
import { useUpdateAgent } from '@/hooks/react-query/agents/use-agents';
|
import { useUpdateAgent, useAgents } from '@/hooks/react-query/agents/use-agents';
|
||||||
import { useUpdateAgentMCPs } from '@/hooks/react-query/agents/use-update-agent-mcps';
|
import { useUpdateAgentMCPs } from '@/hooks/react-query/agents/use-update-agent-mcps';
|
||||||
import { useExportAgent } from '@/hooks/react-query/agents/use-agent-export-import';
|
import { useExportAgent } from '@/hooks/react-query/agents/use-agent-export-import';
|
||||||
import { ExpandableMarkdownEditor } from '@/components/ui/expandable-markdown-editor';
|
import { ExpandableMarkdownEditor } from '@/components/ui/expandable-markdown-editor';
|
||||||
|
@ -62,6 +69,7 @@ interface AgentConfigurationDialogProps {
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
agentId: string;
|
agentId: string;
|
||||||
initialTab?: 'instructions' | 'tools' | 'integrations' | 'knowledge' | 'playbooks' | 'triggers';
|
initialTab?: 'instructions' | 'tools' | 'integrations' | 'knowledge' | 'playbooks' | 'triggers';
|
||||||
|
onAgentChange?: (agentId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AgentConfigurationDialog({
|
export function AgentConfigurationDialog({
|
||||||
|
@ -69,12 +77,15 @@ export function AgentConfigurationDialog({
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
agentId,
|
agentId,
|
||||||
initialTab = 'instructions',
|
initialTab = 'instructions',
|
||||||
|
onAgentChange,
|
||||||
}: AgentConfigurationDialogProps) {
|
}: AgentConfigurationDialogProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { agent, versionData, isViewingOldVersion, isLoading, error } = useAgentVersionData({ agentId });
|
const { agent, versionData, isViewingOldVersion, isLoading, error } = useAgentVersionData({ agentId });
|
||||||
|
const { data: agentsResponse } = useAgents({}, { enabled: !!onAgentChange });
|
||||||
|
const agents = agentsResponse?.agents || [];
|
||||||
|
|
||||||
const updateAgentMutation = useUpdateAgent();
|
const updateAgentMutation = useUpdateAgent();
|
||||||
const updateAgentMCPsMutation = useUpdateAgentMCPs();
|
const updateAgentMCPsMutation = useUpdateAgentMCPs();
|
||||||
|
@ -341,70 +352,150 @@ export function AgentConfigurationDialog({
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div>
|
<div className="flex flex-col gap-2">
|
||||||
{isEditingName ? (
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
{isEditingName ? (
|
||||||
<Input
|
// Name editing mode (takes priority over everything)
|
||||||
ref={nameInputRef}
|
<div className="flex items-center gap-2">
|
||||||
value={editName}
|
<Input
|
||||||
onChange={(e) => setEditName(e.target.value)}
|
ref={nameInputRef}
|
||||||
onKeyDown={(e) => {
|
value={editName}
|
||||||
if (e.key === 'Enter') {
|
onChange={(e) => setEditName(e.target.value)}
|
||||||
handleNameSave();
|
onKeyDown={(e) => {
|
||||||
} else if (e.key === 'Escape') {
|
if (e.key === 'Enter') {
|
||||||
setEditName(formData.name);
|
handleNameSave();
|
||||||
setIsEditingName(false);
|
} else if (e.key === 'Escape') {
|
||||||
}
|
setEditName(formData.name);
|
||||||
}}
|
setIsEditingName(false);
|
||||||
className="h-8 w-64"
|
}
|
||||||
maxLength={50}
|
}}
|
||||||
/>
|
className="h-8 w-64"
|
||||||
<Button
|
maxLength={50}
|
||||||
size="icon"
|
/>
|
||||||
variant="ghost"
|
|
||||||
className="h-8 w-8"
|
|
||||||
onClick={handleNameSave}
|
|
||||||
>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
className="h-8 w-8"
|
|
||||||
onClick={() => {
|
|
||||||
setEditName(formData.name);
|
|
||||||
setIsEditingName(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<DialogTitle className="text-xl font-semibold">
|
|
||||||
{isLoading ? 'Loading...' : formData.name || 'Agent'}
|
|
||||||
</DialogTitle>
|
|
||||||
{isNameEditable && (
|
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-6 w-6"
|
className="h-8 w-8"
|
||||||
|
onClick={handleNameSave}
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsEditingName(true);
|
setEditName(formData.name);
|
||||||
setTimeout(() => {
|
setIsEditingName(false);
|
||||||
nameInputRef.current?.focus();
|
|
||||||
nameInputRef.current?.select();
|
|
||||||
}, 0);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Edit3 className="h-3 w-3" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
</div>
|
) : onAgentChange ? (
|
||||||
)}
|
// When agent switching is enabled, show a sleek inline agent selector
|
||||||
<DialogDescription>
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||||
Configure your agent's capabilities and behavior
|
<DropdownMenu>
|
||||||
</DialogDescription>
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button className="flex items-center gap-2 hover:bg-muted/50 rounded-md px-2 py-1 transition-colors group">
|
||||||
|
<DialogTitle className="text-xl font-semibold truncate">
|
||||||
|
{isLoading ? 'Loading...' : formData.name || 'Agent'}
|
||||||
|
</DialogTitle>
|
||||||
|
<ChevronDown className="h-4 w-4 opacity-60 group-hover:opacity-100 transition-opacity flex-shrink-0" />
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
className="w-80 p-0"
|
||||||
|
align="start"
|
||||||
|
sideOffset={4}
|
||||||
|
>
|
||||||
|
<div className="p-3 border-b">
|
||||||
|
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
||||||
|
<Search className="h-4 w-4" />
|
||||||
|
Switch Agent
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-60 overflow-y-auto">
|
||||||
|
{agents.map((agent: any) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={agent.agent_id}
|
||||||
|
onClick={() => onAgentChange(agent.agent_id)}
|
||||||
|
className="p-3 flex items-center gap-3 cursor-pointer"
|
||||||
|
>
|
||||||
|
{agent.metadata?.is_suna_default ? (
|
||||||
|
<div className="w-6 h-6 rounded-lg bg-muted border flex items-center justify-center flex-shrink-0">
|
||||||
|
<KortixLogo size={12} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<AgentIconAvatar
|
||||||
|
profileImageUrl={agent.profile_image_url}
|
||||||
|
iconName={agent.icon_name}
|
||||||
|
iconColor={agent.icon_color}
|
||||||
|
backgroundColor={agent.icon_background}
|
||||||
|
agentName={agent.name}
|
||||||
|
size={24}
|
||||||
|
className="flex-shrink-0"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="font-medium truncate">{agent.name}</div>
|
||||||
|
{agent.description && (
|
||||||
|
<div className="text-xs text-muted-foreground truncate">
|
||||||
|
{agent.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{agent.agent_id === agentId && (
|
||||||
|
<Check className="h-4 w-4 text-primary flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
{/* Add edit button for name editing when agent switching is enabled */}
|
||||||
|
{isNameEditable && (
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-6 w-6 flex-shrink-0"
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditingName(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
nameInputRef.current?.focus();
|
||||||
|
nameInputRef.current?.select();
|
||||||
|
}, 0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Edit3 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Static title mode (no agent switching available)
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<DialogTitle className="text-xl font-semibold">
|
||||||
|
{isLoading ? 'Loading...' : formData.name || 'Agent'}
|
||||||
|
</DialogTitle>
|
||||||
|
{isNameEditable && (
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-6 w-6"
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditingName(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
nameInputRef.current?.focus();
|
||||||
|
nameInputRef.current?.select();
|
||||||
|
}, 0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Edit3 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -385,6 +385,9 @@ export const AgentsGrid: React.FC<AgentsGridProps> = ({
|
||||||
open={showConfigDialog}
|
open={showConfigDialog}
|
||||||
onOpenChange={setShowConfigDialog}
|
onOpenChange={setShowConfigDialog}
|
||||||
agentId={configAgentId}
|
agentId={configAgentId}
|
||||||
|
onAgentChange={(newAgentId) => {
|
||||||
|
setConfigAgentId(newAgentId);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -433,6 +433,10 @@ export function DashboardContent() {
|
||||||
open={showConfigDialog}
|
open={showConfigDialog}
|
||||||
onOpenChange={setShowConfigDialog}
|
onOpenChange={setShowConfigDialog}
|
||||||
agentId={configAgentId}
|
agentId={configAgentId}
|
||||||
|
onAgentChange={(newAgentId) => {
|
||||||
|
setConfigAgentId(newAgentId);
|
||||||
|
setSelectedAgent(newAgentId);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -667,6 +667,7 @@ export const ChatInput = memo(forwardRef<ChatInputHandles, ChatInputProps>(
|
||||||
onOpenChange={(open) => setAgentConfigDialog({ ...agentConfigDialog, open })}
|
onOpenChange={(open) => setAgentConfigDialog({ ...agentConfigDialog, open })}
|
||||||
agentId={selectedAgentId}
|
agentId={selectedAgentId}
|
||||||
initialTab={agentConfigDialog.tab}
|
initialTab={agentConfigDialog.tab}
|
||||||
|
onAgentChange={onAgentSelect}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -411,6 +411,7 @@ const LoggedInMenu: React.FC<UnifiedConfigMenuProps> = memo(function LoggedInMen
|
||||||
onOpenChange={(open) => setAgentConfigDialog({ ...agentConfigDialog, open })}
|
onOpenChange={(open) => setAgentConfigDialog({ ...agentConfigDialog, open })}
|
||||||
agentId={selectedAgentId || displayAgent?.agent_id}
|
agentId={selectedAgentId || displayAgent?.agent_id}
|
||||||
initialTab={agentConfigDialog.tab}
|
initialTab={agentConfigDialog.tab}
|
||||||
|
onAgentChange={onAgentSelect}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue