diff --git a/frontend/src/app/(dashboard)/dashboard/_components/dashboard-content.tsx b/frontend/src/app/(dashboard)/dashboard/_components/dashboard-content.tsx index 99669248..f2549851 100644 --- a/frontend/src/app/(dashboard)/dashboard/_components/dashboard-content.tsx +++ b/frontend/src/app/(dashboard)/dashboard/_components/dashboard-content.tsx @@ -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() { )} -

Hey, I am

- +

+ {displayName} + {agentAvatar && ( + + {agentAvatar} + + )} +

What would you like to do today?

-
-
- ( subscriptionStatus={subscriptionStatus} canAccessModel={canAccessModel} refreshCustomModels={refreshCustomModels} + + selectedAgentId={selectedAgentId} + onAgentSelect={onAgentSelect} /> diff --git a/frontend/src/components/thread/chat-input/chat-settings-dialog.tsx b/frontend/src/components/thread/chat-input/chat-settings-dialog.tsx new file mode 100644 index 00000000..75628ab1 --- /dev/null +++ b/frontend/src/components/thread/chat-input/chat-settings-dialog.tsx @@ -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 ( + + {controlledOpen === undefined && ( + + + + )} + + + + + + Chat Settings + + + +
+
+ +
+ +
+

+ Choose the AI model that best fits your needs. Premium models offer better performance. +

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/thread/chat-input/chat-settings-dropdown.tsx b/frontend/src/components/thread/chat-input/chat-settings-dropdown.tsx new file mode 100644 index 00000000..a0cfb896 --- /dev/null +++ b/frontend/src/components/thread/chat-input/chat-settings-dropdown.tsx @@ -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: , + category: 'productivity' + }, + { + id: 'sheets', + name: 'Data Analyst', + description: 'Spreadsheet and data analysis expert', + icon: , + 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: , + avatar: selectedRealAgent.avatar + }; + } + if (selectedPredefinedAgent) { + return { + name: selectedPredefinedAgent.name, + icon: selectedPredefinedAgent.icon, + avatar: null + }; + } + return { + name: 'Suna', + icon: , + avatar: null + }; + }; + + const agentDisplay = getAgentDisplay(); + + const AgentCard = ({ agent, isSelected, onClick, type }: { + agent: any; + isSelected: boolean; + onClick: () => void; + type: 'predefined' | 'custom' | 'default'; + }) => ( +
+
+
+ {type === 'default' ? ( + + ) : type === 'custom' ? ( + agent.avatar + ) : ( + agent.icon + )} +
+
+ {agent.name} +
+
+
+ ); + + return ( + <> + + + + + + + + + +

+ {agentDisplay.name} + {agentDisplay.avatar && ` ${agentDisplay.avatar}`} +

+
+
+
+ + +
+
+

Choose Your Agent

+
+ +
+
+
+ +
+ handleAgentSelect(undefined)} + type="default" + /> +
+ {PREDEFINED_AGENTS.map((agent) => ( + handleAgentSelect(agent.id)} + type="predefined" + /> + ))} +
+
+
+
+ +
+ {agentsLoading ? ( +
+ Loading... +
+ ) : agents.length > 0 ? ( +
+ {agents.map((agent) => ( + handleAgentSelect(agent.agent_id)} + type="custom" + /> + ))} +
+ ) : ( +
+ +

No custom agents yet

+ +
+ )} +
+
+ +
+
+ +
+ + +
+
+
+
+ + + ); +} \ No newline at end of file diff --git a/frontend/src/components/thread/chat-input/file-upload-handler.tsx b/frontend/src/components/thread/chat-input/file-upload-handler.tsx index 4485f231..951a17fc 100644 --- a/frontend/src/components/thread/chat-input/file-upload-handler.tsx +++ b/frontend/src/components/thread/chat-input/file-upload-handler.tsx @@ -261,7 +261,6 @@ export const FileUploadHandler = forwardRef< ) : ( )} - Attachments diff --git a/frontend/src/components/thread/chat-input/message-input.tsx b/frontend/src/components/thread/chat-input/message-input.tsx index 97488a89..e356324d 100644 --- a/frontend/src/components/thread/chat-input/message-input.tsx +++ b/frontend/src/components/thread/chat-input/message-input.tsx @@ -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( @@ -73,9 +77,14 @@ export const MessageInput = forwardRef( subscriptionStatus, canAccessModel, refreshCustomModels, + + selectedAgentId, + onAgentSelect, }, ref, ) => { + const { enabled: customAgentsEnabled, loading: flagsLoading } = useFeatureFlag('custom_agents'); + useEffect(() => { const textarea = ref as React.RefObject; if (!textarea.current) return; @@ -147,17 +156,14 @@ export const MessageInput = forwardRef( messages={messages} /> )} - + + {subscriptionStatus === 'no_subscription' && !isLocalMode() &&

Upgrade for full performance

-

The free tier is severely limited by inferior models; upgrade to experience the true full Suna experience.

@@ -165,15 +171,37 @@ export const MessageInput = forwardRef(
} +
- + ) : ( + + )} + + +