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:
marko-kraemer 2025-09-29 14:27:26 +02:00
parent bc6620569f
commit 2c2eaf61f8
5 changed files with 159 additions and 59 deletions

View File

@ -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>

View File

@ -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);
}}
/> />
)} )}
</> </>

View File

@ -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);
}}
/> />
)} )}
</> </>

View File

@ -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>

View File

@ -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}
/> />
)} )}