ui: improved agent selector

This commit is contained in:
Soumyadas15 2025-06-25 13:46:38 +05:30
parent d58c5e2cf1
commit f61e3a7a3b
8 changed files with 481 additions and 26 deletions

View File

@ -25,7 +25,7 @@ import { useAccounts } from '@/hooks/use-accounts';
import { config } from '@/lib/config';
import { useInitiateAgentWithInvalidation } from '@/hooks/react-query/dashboard/use-initiate-agent';
import { ModalProviders } from '@/providers/modal-providers';
import { AgentSelector } from '@/components/dashboard/agent-selector';
import { useAgents } from '@/hooks/react-query/agents/use-agents';
import { cn } from '@/lib/utils';
import { useModal } from '@/hooks/use-modal-store';
import { Examples } from './suggestions/examples';
@ -52,6 +52,20 @@ export function DashboardContent() {
const initiateAgentMutation = useInitiateAgentWithInvalidation();
const { onOpen } = useModal();
// Fetch agents to get the selected agent's name
const { data: agentsResponse } = useAgents({
limit: 100,
sort_by: 'name',
sort_order: 'asc'
});
const agents = agentsResponse?.agents || [];
const selectedAgent = selectedAgentId
? agents.find(agent => agent.agent_id === selectedAgentId)
: null;
const displayName = selectedAgent?.name || 'Suna';
const agentAvatar = selectedAgent?.avatar;
const threadQuery = useThreadQuery(initiatedThreadId || '');
useEffect(() => {
@ -189,24 +203,25 @@ export function DashboardContent() {
</Tooltip>
</div>
)}
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-[650px] max-w-[90%]">
<div className="flex flex-col items-center text-center w-full">
<div className="flex items-center gap-1">
<h1 className="tracking-tight text-4xl text-muted-foreground leading-tight">
Hey, I am
</h1>
<AgentSelector
selectedAgentId={selectedAgentId}
onAgentSelect={setSelectedAgentId}
variant="heading"
/>
<h1 className="tracking-tight text-4xl font-semibold leading-tight text-primary">
{displayName}
{agentAvatar && (
<span className="text-muted-foreground ml-2">
{agentAvatar}
</span>
)}
</h1>
</div>
<p className="tracking-tight text-3xl font-normal text-muted-foreground/80 mt-2">
What would you like to do today?
</p>
</div>
<div className={cn(
"w-full mb-2",
"max-w-full",
@ -220,12 +235,12 @@ export function DashboardContent() {
value={inputValue}
onChange={setInputValue}
hideAttachments={false}
selectedAgentId={selectedAgentId}
onAgentSelect={setSelectedAgentId}
/>
</div>
<Examples onSelectPrompt={setInputValue} />
</div>
<BillingErrorAlert
message={billingError?.message}
currentUsage={billingError?.currentUsage}

View File

@ -28,7 +28,7 @@ import { UnifiedMessage, ApiMessageType, ToolCallInput, Project } from '../_type
import { useThreadData, useToolCalls, useBilling, useKeyboardShortcuts } from '../_hooks';
import { ThreadError, UpgradeDialog, ThreadLayout } from '../_components';
import { useVncPreloader } from '@/hooks/useVncPreloader';
import { useAgent } from '@/hooks/react-query/agents/use-agents';
import { useThreadAgent } from '@/hooks/react-query/agents/use-agents';
export default function ThreadPage({
params,
@ -124,7 +124,8 @@ export default function ThreadPage({
const addUserMessageMutation = useAddUserMessageMutation();
const startAgentMutation = useStartAgentMutation();
const stopAgentMutation = useStopAgentMutation();
const { data: agent } = useAgent(threadQuery.data?.agent_id);
const { data: threadAgentData } = useThreadAgent(threadId);
const agent = threadAgentData?.agent;
const workflowId = threadQuery.data?.metadata?.workflow_id;
const { data: subscriptionData } = useSubscription();

View File

@ -288,6 +288,9 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
subscriptionStatus={subscriptionStatus}
canAccessModel={canAccessModel}
refreshCustomModels={refreshCustomModels}
selectedAgentId={selectedAgentId}
onAgentSelect={onAgentSelect}
/>
</CardContent>
</div>

View File

@ -0,0 +1,99 @@
'use client';
import React, { useState } from 'react';
import { Settings, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
import { ModelSelector } from './model-selector';
import { SubscriptionStatus } from './_use-model-selection';
import { cn } from '@/lib/utils';
interface ChatSettingsDialogProps {
selectedModel: string;
onModelChange: (model: string) => void;
modelOptions: any[];
subscriptionStatus: SubscriptionStatus;
canAccessModel: (modelId: string) => boolean;
refreshCustomModels?: () => void;
disabled?: boolean;
className?: string;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
export function ChatSettingsDialog({
selectedModel,
onModelChange,
modelOptions,
subscriptionStatus,
canAccessModel,
refreshCustomModels,
disabled = false,
className,
open: controlledOpen,
onOpenChange: controlledOnOpenChange,
}: ChatSettingsDialogProps) {
const [internalOpen, setInternalOpen] = useState(false);
const open = controlledOpen !== undefined ? controlledOpen : internalOpen;
const setOpen = controlledOnOpenChange || setInternalOpen;
return (
<Dialog open={open} onOpenChange={setOpen}>
{controlledOpen === undefined && (
<DialogTrigger asChild>
<Button
variant="ghost"
size="sm"
className={cn(
'h-8 w-8 p-0 text-muted-foreground hover:text-foreground',
'rounded-lg',
className
)}
disabled={disabled}
>
<Settings className="h-4 w-4" />
</Button>
</DialogTrigger>
)}
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Settings className="h-5 w-5" />
Chat Settings
</DialogTitle>
</DialogHeader>
<div className="space-y-6 py-4">
<div className="space-y-2">
<Label htmlFor="model-selector" className="text-sm font-medium">
AI Model
</Label>
<div className="w-full">
<ModelSelector
selectedModel={selectedModel}
onModelChange={onModelChange}
modelOptions={modelOptions}
subscriptionStatus={subscriptionStatus}
canAccessModel={canAccessModel}
refreshCustomModels={refreshCustomModels}
hasBorder={true}
/>
</div>
<p className="text-xs text-muted-foreground">
Choose the AI model that best fits your needs. Premium models offer better performance.
</p>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,308 @@
'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 { Button } from '@/components/ui/button';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
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 { useRouter } from 'next/navigation';
import { cn } from '@/lib/utils';
import { Label } from '@/components/ui/label';
interface PredefinedAgent {
id: string;
name: string;
description: string;
icon: React.ReactNode;
category: 'productivity' | 'creative' | 'development';
}
const PREDEFINED_AGENTS: PredefinedAgent[] = [
{
id: 'slides',
name: 'Slides Pro',
description: 'Create stunning presentations and slide decks',
icon: <Presentation className="h-4 w-4 mt-1" />,
category: 'productivity'
},
{
id: 'sheets',
name: 'Data Analyst',
description: 'Spreadsheet and data analysis expert',
icon: <FileSpreadsheet className="h-4 w-4 mt-1" />,
category: 'productivity'
}
];
interface ChatSettingsDropdownProps {
selectedAgentId?: string;
onAgentSelect?: (agentId: string | undefined) => void;
selectedModel: string;
onModelChange: (model: string) => void;
modelOptions: any[];
subscriptionStatus: SubscriptionStatus;
canAccessModel: (modelId: string) => boolean;
refreshCustomModels?: () => void;
disabled?: boolean;
className?: string;
}
export function ChatSettingsDropdown({
selectedAgentId,
onAgentSelect,
selectedModel,
onModelChange,
modelOptions,
subscriptionStatus,
canAccessModel,
refreshCustomModels,
disabled = false,
className,
}: ChatSettingsDropdownProps) {
const [dropdownOpen, setDropdownOpen] = useState(false);
const [dialogOpen, setDialogOpen] = useState(false);
const router = useRouter();
// Check if custom agents feature is enabled
const { enabled: customAgentsEnabled, loading: flagsLoading } = useFeatureFlag('custom_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'
});
const agents = (customAgentsEnabled && agentsResponse?.agents) || [];
const defaultAgent = agents.find(agent => agent.is_default);
// 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');
};
const getAgentDisplay = () => {
if (selectedRealAgent) {
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
};
}
return {
name: 'Suna',
icon: <User className="h-4 w-4" />,
avatar: null
};
};
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}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="sm"
className={cn(
'h-8 w-8 p-0 text-muted-foreground hover:text-foreground relative',
'rounded-lg',
className
)}
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>
{agentDisplay.name}
{agentDisplay.avatar && ` ${agentDisplay.avatar}`}
</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"
/>
<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>
{agentsLoading ? (
<div className="flex items-center justify-center py-8 text-sm text-muted-foreground">
Loading...
</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"
/>
))}
</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"
>
<Plus className="h-3 w-3 mr-1" />
Get Started
</Button>
</div>
)}
</div>
</div>
<Separator className="my-4" />
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={handleExploreAll}
className="text-xs"
>
<Search className="h-3 w-3" />
Explore All
</Button>
</div>
<Button
variant="ghost"
size="sm"
onClick={handleMoreOptions}
className="text-xs"
>
<Settings className="h-3 w-3" />
More Options
<ChevronRight className="h-3 w-3" />
</Button>
</div>
</div>
</PopoverContent>
</Popover>
<ChatSettingsDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
selectedModel={selectedModel}
onModelChange={onModelChange}
modelOptions={modelOptions}
subscriptionStatus={subscriptionStatus}
canAccessModel={canAccessModel}
refreshCustomModels={refreshCustomModels}
/>
</>
);
}

View File

@ -261,7 +261,6 @@ export const FileUploadHandler = forwardRef<
) : (
<Paperclip className="h-4 w-4" />
)}
<span className="text-sm sm:block hidden">Attachments</span>
</Button>
</TooltipTrigger>
<TooltipContent side="top">

View File

@ -7,8 +7,10 @@ import { UploadedFile } from './chat-input';
import { FileUploadHandler } from './file-upload-handler';
import { VoiceRecorder } from './voice-recorder';
import { ModelSelector } from './model-selector';
import { ChatSettingsDropdown } from './chat-settings-dropdown';
import { SubscriptionStatus } from './_use-model-selection';
import { isLocalMode } from '@/lib/config';
import { useFeatureFlag } from '@/lib/feature-flags';
import { TooltipContent } from '@/components/ui/tooltip';
import { Tooltip } from '@/components/ui/tooltip';
import { TooltipProvider, TooltipTrigger } from '@radix-ui/react-tooltip';
@ -41,6 +43,8 @@ interface MessageInputProps {
subscriptionStatus: SubscriptionStatus;
canAccessModel: (modelId: string) => boolean;
refreshCustomModels?: () => void;
selectedAgentId?: string;
onAgentSelect?: (agentId: string | undefined) => void;
}
export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
@ -73,9 +77,14 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
subscriptionStatus,
canAccessModel,
refreshCustomModels,
selectedAgentId,
onAgentSelect,
},
ref,
) => {
const { enabled: customAgentsEnabled, loading: flagsLoading } = useFeatureFlag('custom_agents');
useEffect(() => {
const textarea = ref as React.RefObject<HTMLTextAreaElement>;
if (!textarea.current) return;
@ -147,17 +156,14 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
messages={messages}
/>
)}
<VoiceRecorder
onTranscription={onTranscription}
disabled={loading || (disabled && !isAgentRunning)}
/>
</div>
{subscriptionStatus === 'no_subscription' && !isLocalMode() &&
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<p className='text-sm text-amber-500 hidden sm:block'>Upgrade for full performance</p>
</TooltipTrigger>
<TooltipContent>
<p>The free tier is severely limited by inferior models; upgrade to experience the true full Suna experience.</p>
@ -165,15 +171,37 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
</Tooltip>
</TooltipProvider>
}
<div className='flex items-center gap-2'>
<ModelSelector
selectedModel={selectedModel}
onModelChange={onModelChange}
modelOptions={modelOptions}
subscriptionStatus={subscriptionStatus}
canAccessModel={canAccessModel}
refreshCustomModels={refreshCustomModels}
{/* Show model selector inline if custom agents are disabled, otherwise show settings dropdown */}
{!customAgentsEnabled || flagsLoading ? (
<ModelSelector
selectedModel={selectedModel}
onModelChange={onModelChange}
modelOptions={modelOptions}
subscriptionStatus={subscriptionStatus}
canAccessModel={canAccessModel}
refreshCustomModels={refreshCustomModels}
/>
) : (
<ChatSettingsDropdown
selectedAgentId={selectedAgentId}
onAgentSelect={onAgentSelect}
selectedModel={selectedModel}
onModelChange={onModelChange}
modelOptions={modelOptions}
subscriptionStatus={subscriptionStatus}
canAccessModel={canAccessModel}
refreshCustomModels={refreshCustomModels}
disabled={loading || (disabled && !isAgentRunning)}
/>
)}
<VoiceRecorder
onTranscription={onTranscription}
disabled={loading || (disabled && !isAgentRunning)}
/>
<Button
type="submit"
onClick={isAgentRunning && onStopAgent ? onStopAgent : onSubmit}

View File

@ -46,6 +46,7 @@ interface ModelSelectorProps {
canAccessModel: (modelId: string) => boolean;
subscriptionStatus: SubscriptionStatus;
refreshCustomModels?: () => void;
hasBorder?: boolean;
}
export const ModelSelector: React.FC<ModelSelectorProps> = ({
@ -55,6 +56,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
canAccessModel,
subscriptionStatus,
refreshCustomModels,
hasBorder = false,
}) => {
const [paywallOpen, setPaywallOpen] = useState(false);
const [billingModalOpen, setBillingModalOpen] = useState(false);
@ -508,7 +510,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
variant={hasBorder ? "outline" : "ghost"}
size="default"
className="h-8 rounded-lg text-muted-foreground shadow-none border-none focus:ring-0 px-3"
>