chore(ui): make agent selector simpler

This commit is contained in:
Soumyadas15 2025-06-26 14:40:13 +05:30
parent 23fa2ba4e8
commit 06b0035b51
2 changed files with 210 additions and 196 deletions

View File

@ -209,7 +209,7 @@ export function DashboardContent() {
<h1 className="tracking-tight text-4xl text-muted-foreground leading-tight"> <h1 className="tracking-tight text-4xl text-muted-foreground leading-tight">
Hey, I am Hey, I am
</h1> </h1>
<h1 className="tracking-tight text-4xl font-semibold leading-tight text-primary"> <h1 className="ml-1 tracking-tight text-4xl font-semibold leading-tight text-primary">
{displayName} {displayName}
{agentAvatar && ( {agentAvatar && (
<span className="text-muted-foreground ml-2"> <span className="text-muted-foreground ml-2">

View File

@ -1,28 +1,25 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { Settings, ChevronRight, Bot, Presentation, Video, Code, FileSpreadsheet, Search, Plus, Star, User, Sparkles, Database, MessageSquare, Calculator, Palette, Zap } from 'lucide-react'; import { Settings, ChevronRight, Bot, Presentation, FileSpreadsheet, Search, Plus, User, Check, ChevronDown } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Popover, DropdownMenu,
PopoverContent, DropdownMenuContent,
PopoverTrigger, DropdownMenuItem,
} from '@/components/ui/popover'; DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip'; } from '@/components/ui/tooltip';
import { ChatSettingsDialog } from './chat-settings-dialog';
import { SubscriptionStatus } from './_use-model-selection';
import { useAgents } from '@/hooks/react-query/agents/use-agents'; import { useAgents } from '@/hooks/react-query/agents/use-agents';
import { useFeatureFlag } from '@/lib/feature-flags'; import { ChatSettingsDialog } from './chat-settings-dialog';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Label } from '@/components/ui/label';
interface PredefinedAgent { interface PredefinedAgent {
id: string; id: string;
@ -35,16 +32,16 @@ interface PredefinedAgent {
const PREDEFINED_AGENTS: PredefinedAgent[] = [ const PREDEFINED_AGENTS: PredefinedAgent[] = [
{ {
id: 'slides', id: 'slides',
name: 'Slides Pro', name: 'Slides',
description: 'Create stunning presentations and slide decks', description: 'Create stunning presentations and slide decks',
icon: <Presentation className="h-4 w-4 mt-1" />, icon: <Presentation className="h-4 w-4" />,
category: 'productivity' category: 'productivity'
}, },
{ {
id: 'sheets', id: 'sheets',
name: 'Data Analyst', name: 'Sheets',
description: 'Spreadsheet and data analysis expert', description: 'Spreadsheet and data analysis expert',
icon: <FileSpreadsheet className="h-4 w-4 mt-1" />, icon: <FileSpreadsheet className="h-4 w-4" />,
category: 'productivity' category: 'productivity'
} }
]; ];
@ -55,14 +52,13 @@ interface ChatSettingsDropdownProps {
selectedModel: string; selectedModel: string;
onModelChange: (model: string) => void; onModelChange: (model: string) => void;
modelOptions: any[]; modelOptions: any[];
subscriptionStatus: SubscriptionStatus; subscriptionStatus: any;
canAccessModel: (modelId: string) => boolean; canAccessModel: (modelId: string) => boolean;
refreshCustomModels?: () => void; refreshCustomModels?: () => void;
disabled?: boolean; disabled?: boolean;
className?: string;
} }
export function ChatSettingsDropdown({ export const ChatSettingsDropdown: React.FC<ChatSettingsDropdownProps> = ({
selectedAgentId, selectedAgentId,
onAgentSelect, onAgentSelect,
selectedModel, selectedModel,
@ -72,227 +68,245 @@ export function ChatSettingsDropdown({
canAccessModel, canAccessModel,
refreshCustomModels, refreshCustomModels,
disabled = false, disabled = false,
className, }) => {
}: ChatSettingsDropdownProps) { const [isOpen, setIsOpen] = useState(false);
const [dropdownOpen, setDropdownOpen] = useState(false); const [searchQuery, setSearchQuery] = useState('');
const [highlightedIndex, setHighlightedIndex] = useState<number>(-1);
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const searchInputRef = useRef<HTMLInputElement>(null);
const router = useRouter(); const router = useRouter();
// Check if custom agents feature is enabled const { data: agentsResponse, isLoading: agentsLoading } = useAgents();
const { enabled: customAgentsEnabled, loading: flagsLoading } = useFeatureFlag('custom_agents'); const agents = agentsResponse?.agents || [];
// Fetch real agents from API only if feature is enabled // Combine all agents
const { data: agentsResponse, isLoading: agentsLoading, refetch: loadAgents } = useAgents({ const allAgents = [
limit: 100, {
sort_by: 'name', id: undefined,
sort_order: 'asc' name: 'Suna',
}); description: 'Your personal AI assistant',
type: 'default' as const,
icon: <User className="h-4 w-4" />
},
...PREDEFINED_AGENTS.map(agent => ({
...agent,
type: 'predefined' as const
})),
...agents.map((agent: any) => ({
...agent,
id: agent.agent_id,
type: 'custom' as const,
icon: agent.avatar || <Bot className="h-4 w-4" />
}))
];
const agents = (customAgentsEnabled && agentsResponse?.agents) || []; // Filter agents based on search query
const defaultAgent = agents.find(agent => agent.is_default); const filteredAgents = allAgents.filter((agent) =>
agent.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
agent.description?.toLowerCase().includes(searchQuery.toLowerCase())
);
// Find selected agent - could be from real agents or predefined useEffect(() => {
const selectedRealAgent = agents.find(a => a.agent_id === selectedAgentId); if (isOpen && searchInputRef.current) {
const selectedPredefinedAgent = PREDEFINED_AGENTS.find(a => a.id === selectedAgentId); setTimeout(() => {
const selectedAgent = selectedRealAgent || selectedPredefinedAgent; searchInputRef.current?.focus();
}, 50);
const handleAgentSelect = (agentId: string | undefined) => { } else {
onAgentSelect?.(agentId); setSearchQuery('');
setDropdownOpen(false); setHighlightedIndex(-1);
}; }
}, [isOpen]);
const handleMoreOptions = () => {
setDropdownOpen(false);
setDialogOpen(true);
};
const handleExploreAll = () => {
setDropdownOpen(false);
router.push('/agents');
};
const getAgentDisplay = () => { const getAgentDisplay = () => {
if (selectedRealAgent) { const selectedAgent = allAgents.find(agent => agent.id === selectedAgentId);
if (selectedAgent) {
return { return {
name: selectedRealAgent.name, name: selectedAgent.name,
icon: <Bot className="h-4 w-4" />, icon: selectedAgent.icon
avatar: selectedRealAgent.avatar
};
}
if (selectedPredefinedAgent) {
return {
name: selectedPredefinedAgent.name,
icon: selectedPredefinedAgent.icon,
avatar: null
}; };
} }
return { return {
name: 'Suna', name: 'Suna',
icon: <User className="h-4 w-4" />, icon: <User className="h-4 w-4" />
avatar: null
}; };
}; };
const handleAgentSelect = (agentId: string | undefined) => {
onAgentSelect?.(agentId);
setIsOpen(false);
};
const handleSearchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
e.stopPropagation();
if (e.key === 'ArrowDown') {
e.preventDefault();
setHighlightedIndex((prev) =>
prev < filteredAgents.length - 1 ? prev + 1 : 0
);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setHighlightedIndex((prev) =>
prev > 0 ? prev - 1 : filteredAgents.length - 1
);
} else if (e.key === 'Enter' && highlightedIndex >= 0) {
e.preventDefault();
const selectedAgent = filteredAgents[highlightedIndex];
if (selectedAgent) {
handleAgentSelect(selectedAgent.id);
}
}
};
const handleExploreAll = () => {
setIsOpen(false);
router.push('/agents');
};
const handleMoreOptions = () => {
setIsOpen(false);
setDialogOpen(true);
};
const agentDisplay = getAgentDisplay(); const agentDisplay = getAgentDisplay();
const AgentCard = ({ agent, isSelected, onClick, type }: {
agent: any;
isSelected: boolean;
onClick: () => void;
type: 'predefined' | 'custom' | 'default';
}) => (
<div
onClick={onClick}
className={cn(
"group relative rounded-lg p-1.5 cursor-pointer transition-all",
"hover:bg-accent/50",
isSelected ? "bg-accent border-accent-foreground/50" : ""
)}
>
<div className="flex items-start gap-2">
<div className="flex-shrink-0">
{type === 'default' ? (
<User className="h-4 w-4 mt-1" />
) : type === 'custom' ? (
agent.avatar
) : (
agent.icon
)}
</div>
<div className="flex-1 min-w-0">
<span className="font-medium text-sm truncate">{agent.name}</span>
</div>
</div>
</div>
);
return ( return (
<> <>
<Popover open={dropdownOpen} onOpenChange={setDropdownOpen}> <DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<PopoverTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className={cn( className="h-8 px-2 text-xs font-medium"
'h-8 w-8 p-0 text-muted-foreground hover:text-foreground relative',
'rounded-lg',
className
)}
disabled={disabled} disabled={disabled}
> >
<Settings className="h-4 w-4" /> <div className="flex items-center gap-1.5">
{selectedAgentId && ( {agentDisplay.icon}
<div className="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-primary" /> <span className="hidden sm:inline-block truncate max-w-[80px]">
)} {agentDisplay.name}
</span>
<ChevronDown className="h-3 w-3 opacity-50" />
</div>
</Button> </Button>
</PopoverTrigger> </DropdownMenuTrigger>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent>
<p> <p>Select Agent</p>
{agentDisplay.name}
{agentDisplay.avatar && ` ${agentDisplay.avatar}`}
</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
<PopoverContent align="end" className="w-[480px] p-0" sideOffset={4}> <DropdownMenuContent align="end" className="w-80 p-0" sideOffset={4}>
<div className="p-4"> <div className="p-3 border-b">
<div className="mb-4"> <div className="relative">
<h3 className="text-sm font-medium">Choose Your Agent</h3> <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<input
ref={searchInputRef}
type="text"
placeholder="Search agents..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleSearchInputKeyDown}
className="w-full pl-8 pr-3 py-2 text-sm bg-transparent border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
/>
</div> </div>
</div>
<div className="max-h-80 overflow-y-auto">
{agentsLoading ? (
<div className="p-3 text-sm text-muted-foreground text-center">
Loading agents...
</div>
) : filteredAgents.length === 0 ? (
<div className="p-3 text-sm text-muted-foreground text-center">
No agents found
</div>
) : (
filteredAgents.map((agent, index) => {
const isSelected = agent.id === selectedAgentId;
const isHighlighted = index === highlightedIndex;
<div className="grid grid-cols-2 gap-4"> return (
<div className="space-y-1"> <TooltipProvider key={agent.id || 'default'}>
<div className="flex items-center gap-2 mb-2"> <Tooltip>
<Label className="text-xs text-muted-foreground font-medium">Default Agents</Label> <TooltipTrigger asChild>
</div> <div className="w-full">
<AgentCard <DropdownMenuItem
agent={{ name: 'Suna', description: 'Your personal AI assistant' }} className={cn(
isSelected={!selectedAgentId} "text-sm px-3 py-3 mx-2 my-0.5 flex items-center justify-between cursor-pointer",
onClick={() => handleAgentSelect(undefined)} isHighlighted && "bg-accent",
type="default" )}
/> onClick={() => handleAgentSelect(agent.id)}
<div className="space-y-1 max-h-[300px] overflow-y-auto"> onMouseEnter={() => setHighlightedIndex(index)}
{PREDEFINED_AGENTS.map((agent) => ( >
<AgentCard <div className="flex items-center gap-3">
key={agent.id} <div className="flex-shrink-0">
agent={agent} {agent.icon}
isSelected={selectedAgentId === agent.id} </div>
onClick={() => handleAgentSelect(agent.id)} <div className="flex-1 min-w-0">
type="predefined" <div className="flex items-center gap-2">
/> <span className="font-medium text-sm truncate">
))} {agent.name}
</div> </span>
</div> {agent.type === 'predefined' && (
<div className="space-y-3"> <Badge variant="secondary" className="text-xs px-1 py-0 h-4">
<div className="flex items-center gap-2 mb-2"> Pro
<Label className="text-xs text-muted-foreground font-medium">Your Agents</Label> </Badge>
</div> )}
{agentsLoading ? ( {agent.type === 'custom' && (
<div className="flex items-center justify-center py-8 text-sm text-muted-foreground"> <Badge variant="outline" className="text-xs px-1 py-0 h-4">
Loading... Custom
</div> </Badge>
) : agents.length > 0 ? ( )}
<div className="space-y-1 max-h-[300px] overflow-y-auto"> </div>
{agents.map((agent) => ( <p className="text-xs text-muted-foreground line-clamp-1 mt-0.5">
<AgentCard {agent.description}
key={agent.agent_id} </p>
agent={agent} </div>
isSelected={selectedAgentId === agent.agent_id} </div>
onClick={() => handleAgentSelect(agent.agent_id)} {isSelected && (
type="custom" <Check className="h-4 w-4 text-blue-500 flex-shrink-0" />
/> )}
))} </DropdownMenuItem>
</div> </div>
) : ( </TooltipTrigger>
<div className="flex flex-col items-center justify-center py-8 text-center"> <TooltipContent side="left" className="text-xs max-w-xs">
<Bot className="h-8 w-8 text-muted-foreground/50 mb-2" /> <p>{agent.description}</p>
<p className="text-sm text-muted-foreground mb-2">No custom agents yet</p> </TooltipContent>
<Button </Tooltip>
variant="outline" </TooltipProvider>
size="sm" );
onClick={handleExploreAll} })
className="text-xs" )}
> </div>
<Plus className="h-3 w-3 mr-1" /> <div className="border-t p-3">
Get Started
</Button>
</div>
)}
</div>
</div>
<Separator className="my-4" />
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex gap-2"> <Button
<Button variant="outline"
variant="outline" size="sm"
size="sm" onClick={handleExploreAll}
onClick={handleExploreAll} className="text-xs"
className="text-xs" >
> <Search className="h-3 w-3" />
<Search className="h-3 w-3" /> Explore All
Explore All </Button>
</Button>
</div>
<Button <Button
variant="ghost" variant="outline"
size="sm" size="sm"
onClick={handleMoreOptions} onClick={handleMoreOptions}
className="text-xs" className="text-xs"
> >
<Settings className="h-3 w-3" /> <Settings className="h-3 w-3" />
More Options More Options
<ChevronRight className="h-3 w-3" /> <ChevronRight className="h-3 w-3 ml-1" />
</Button> </Button>
</div> </div>
</div> </div>
</PopoverContent> </DropdownMenuContent>
</Popover> </DropdownMenu>
<ChatSettingsDialog <ChatSettingsDialog
open={dialogOpen} open={dialogOpen}
onOpenChange={setDialogOpen} onOpenChange={setDialogOpen}
@ -305,4 +319,4 @@ export function ChatSettingsDropdown({
/> />
</> </>
); );
} };