mirror of https://github.com/kortix-ai/suna.git
chore(ui): make agent selector simpler
This commit is contained in:
parent
23fa2ba4e8
commit
06b0035b51
|
@ -209,7 +209,7 @@ export function DashboardContent() {
|
|||
<h1 className="tracking-tight text-4xl text-muted-foreground leading-tight">
|
||||
Hey, I am
|
||||
</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}
|
||||
{agentAvatar && (
|
||||
<span className="text-muted-foreground ml-2">
|
||||
|
|
|
@ -1,28 +1,25 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Settings, ChevronRight, Bot, Presentation, Video, Code, FileSpreadsheet, Search, Plus, Star, User, Sparkles, Database, MessageSquare, Calculator, Palette, Zap } from 'lucide-react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Settings, ChevronRight, Bot, Presentation, FileSpreadsheet, Search, Plus, User, Check, ChevronDown } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} 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 { useFeatureFlag } from '@/lib/feature-flags';
|
||||
import { ChatSettingsDialog } from './chat-settings-dialog';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
interface PredefinedAgent {
|
||||
id: string;
|
||||
|
@ -35,16 +32,16 @@ interface PredefinedAgent {
|
|||
const PREDEFINED_AGENTS: PredefinedAgent[] = [
|
||||
{
|
||||
id: 'slides',
|
||||
name: 'Slides Pro',
|
||||
name: 'Slides',
|
||||
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'
|
||||
},
|
||||
{
|
||||
id: 'sheets',
|
||||
name: 'Data Analyst',
|
||||
name: 'Sheets',
|
||||
description: 'Spreadsheet and data analysis expert',
|
||||
icon: <FileSpreadsheet className="h-4 w-4 mt-1" />,
|
||||
icon: <FileSpreadsheet className="h-4 w-4" />,
|
||||
category: 'productivity'
|
||||
}
|
||||
];
|
||||
|
@ -55,14 +52,13 @@ interface ChatSettingsDropdownProps {
|
|||
selectedModel: string;
|
||||
onModelChange: (model: string) => void;
|
||||
modelOptions: any[];
|
||||
subscriptionStatus: SubscriptionStatus;
|
||||
subscriptionStatus: any;
|
||||
canAccessModel: (modelId: string) => boolean;
|
||||
refreshCustomModels?: () => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ChatSettingsDropdown({
|
||||
export const ChatSettingsDropdown: React.FC<ChatSettingsDropdownProps> = ({
|
||||
selectedAgentId,
|
||||
onAgentSelect,
|
||||
selectedModel,
|
||||
|
@ -72,202 +68,220 @@ export function ChatSettingsDropdown({
|
|||
canAccessModel,
|
||||
refreshCustomModels,
|
||||
disabled = false,
|
||||
className,
|
||||
}: ChatSettingsDropdownProps) {
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [highlightedIndex, setHighlightedIndex] = useState<number>(-1);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const router = useRouter();
|
||||
|
||||
// Check if custom agents feature is enabled
|
||||
const { enabled: customAgentsEnabled, loading: flagsLoading } = useFeatureFlag('custom_agents');
|
||||
const { data: agentsResponse, isLoading: agentsLoading } = useAgents();
|
||||
const agents = agentsResponse?.agents || [];
|
||||
|
||||
// Fetch real agents from API only if feature is enabled
|
||||
const { data: agentsResponse, isLoading: agentsLoading, refetch: loadAgents } = useAgents({
|
||||
limit: 100,
|
||||
sort_by: 'name',
|
||||
sort_order: 'asc'
|
||||
});
|
||||
// Combine all agents
|
||||
const allAgents = [
|
||||
{
|
||||
id: undefined,
|
||||
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) || [];
|
||||
const defaultAgent = agents.find(agent => agent.is_default);
|
||||
// Filter agents based on search query
|
||||
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
|
||||
const selectedRealAgent = agents.find(a => a.agent_id === selectedAgentId);
|
||||
const selectedPredefinedAgent = PREDEFINED_AGENTS.find(a => a.id === selectedAgentId);
|
||||
const selectedAgent = selectedRealAgent || selectedPredefinedAgent;
|
||||
|
||||
const handleAgentSelect = (agentId: string | undefined) => {
|
||||
onAgentSelect?.(agentId);
|
||||
setDropdownOpen(false);
|
||||
};
|
||||
|
||||
const handleMoreOptions = () => {
|
||||
setDropdownOpen(false);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleExploreAll = () => {
|
||||
setDropdownOpen(false);
|
||||
router.push('/agents');
|
||||
};
|
||||
useEffect(() => {
|
||||
if (isOpen && searchInputRef.current) {
|
||||
setTimeout(() => {
|
||||
searchInputRef.current?.focus();
|
||||
}, 50);
|
||||
} else {
|
||||
setSearchQuery('');
|
||||
setHighlightedIndex(-1);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const getAgentDisplay = () => {
|
||||
if (selectedRealAgent) {
|
||||
const selectedAgent = allAgents.find(agent => agent.id === selectedAgentId);
|
||||
if (selectedAgent) {
|
||||
return {
|
||||
name: selectedRealAgent.name,
|
||||
icon: <Bot className="h-4 w-4" />,
|
||||
avatar: selectedRealAgent.avatar
|
||||
};
|
||||
}
|
||||
if (selectedPredefinedAgent) {
|
||||
return {
|
||||
name: selectedPredefinedAgent.name,
|
||||
icon: selectedPredefinedAgent.icon,
|
||||
avatar: null
|
||||
name: selectedAgent.name,
|
||||
icon: selectedAgent.icon
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: 'Suna',
|
||||
icon: <User className="h-4 w-4" />,
|
||||
avatar: null
|
||||
icon: <User className="h-4 w-4" />
|
||||
};
|
||||
};
|
||||
|
||||
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 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 (
|
||||
<>
|
||||
<Popover open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
||||
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<PopoverTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(
|
||||
'h-8 w-8 p-0 text-muted-foreground hover:text-foreground relative',
|
||||
'rounded-lg',
|
||||
className
|
||||
)}
|
||||
className="h-8 px-2 text-xs font-medium"
|
||||
disabled={disabled}
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
{selectedAgentId && (
|
||||
<div className="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-primary" />
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<p>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{agentDisplay.icon}
|
||||
<span className="hidden sm:inline-block truncate max-w-[80px]">
|
||||
{agentDisplay.name}
|
||||
{agentDisplay.avatar && ` ${agentDisplay.avatar}`}
|
||||
</p>
|
||||
</span>
|
||||
<ChevronDown className="h-3 w-3 opacity-50" />
|
||||
</div>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Select Agent</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<PopoverContent align="end" className="w-[480px] p-0" sideOffset={4}>
|
||||
<div className="p-4">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-sm font-medium">Choose Your Agent</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Label className="text-xs text-muted-foreground font-medium">Default Agents</Label>
|
||||
</div>
|
||||
<AgentCard
|
||||
agent={{ name: 'Suna', description: 'Your personal AI assistant' }}
|
||||
isSelected={!selectedAgentId}
|
||||
onClick={() => handleAgentSelect(undefined)}
|
||||
type="default"
|
||||
<DropdownMenuContent align="end" className="w-80 p-0" sideOffset={4}>
|
||||
<div className="p-3 border-b">
|
||||
<div className="relative">
|
||||
<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 className="space-y-1 max-h-[300px] overflow-y-auto">
|
||||
{PREDEFINED_AGENTS.map((agent) => (
|
||||
<AgentCard
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
isSelected={selectedAgentId === agent.id}
|
||||
onClick={() => handleAgentSelect(agent.id)}
|
||||
type="predefined"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Label className="text-xs text-muted-foreground font-medium">Your Agents</Label>
|
||||
</div>
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{agentsLoading ? (
|
||||
<div className="flex items-center justify-center py-8 text-sm text-muted-foreground">
|
||||
Loading...
|
||||
<div className="p-3 text-sm text-muted-foreground text-center">
|
||||
Loading agents...
|
||||
</div>
|
||||
) : agents.length > 0 ? (
|
||||
<div className="space-y-1 max-h-[300px] overflow-y-auto">
|
||||
{agents.map((agent) => (
|
||||
<AgentCard
|
||||
key={agent.agent_id}
|
||||
agent={agent}
|
||||
isSelected={selectedAgentId === agent.agent_id}
|
||||
onClick={() => handleAgentSelect(agent.agent_id)}
|
||||
type="custom"
|
||||
/>
|
||||
))}
|
||||
) : filteredAgents.length === 0 ? (
|
||||
<div className="p-3 text-sm text-muted-foreground text-center">
|
||||
No agents found
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||
<Bot className="h-8 w-8 text-muted-foreground/50 mb-2" />
|
||||
<p className="text-sm text-muted-foreground mb-2">No custom agents yet</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleExploreAll}
|
||||
className="text-xs"
|
||||
filteredAgents.map((agent, index) => {
|
||||
const isSelected = agent.id === selectedAgentId;
|
||||
const isHighlighted = index === highlightedIndex;
|
||||
|
||||
return (
|
||||
<TooltipProvider key={agent.id || 'default'}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="w-full">
|
||||
<DropdownMenuItem
|
||||
className={cn(
|
||||
"text-sm px-3 py-3 mx-2 my-0.5 flex items-center justify-between cursor-pointer",
|
||||
isHighlighted && "bg-accent",
|
||||
)}
|
||||
onClick={() => handleAgentSelect(agent.id)}
|
||||
onMouseEnter={() => setHighlightedIndex(index)}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
Get Started
|
||||
</Button>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
{agent.icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm truncate">
|
||||
{agent.name}
|
||||
</span>
|
||||
{agent.type === 'predefined' && (
|
||||
<Badge variant="secondary" className="text-xs px-1 py-0 h-4">
|
||||
Pro
|
||||
</Badge>
|
||||
)}
|
||||
{agent.type === 'custom' && (
|
||||
<Badge variant="outline" className="text-xs px-1 py-0 h-4">
|
||||
Custom
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground line-clamp-1 mt-0.5">
|
||||
{agent.description}
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="my-4" />
|
||||
</div>
|
||||
{isSelected && (
|
||||
<Check className="h-4 w-4 text-blue-500 flex-shrink-0" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" className="text-xs max-w-xs">
|
||||
<p>{agent.description}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
<div className="border-t p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
@ -277,22 +291,22 @@ export function ChatSettingsDropdown({
|
|||
<Search className="h-3 w-3" />
|
||||
Explore All
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleMoreOptions}
|
||||
className="text-xs"
|
||||
>
|
||||
<Settings className="h-3 w-3" />
|
||||
More Options
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
<ChevronRight className="h-3 w-3 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<ChatSettingsDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
|
@ -305,4 +319,4 @@ export function ChatSettingsDropdown({
|
|||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue