This commit is contained in:
marko-kraemer 2025-07-10 07:21:23 +02:00
parent 37c781a99b
commit 87da181fc5
13 changed files with 255 additions and 521 deletions

View File

@ -377,7 +377,7 @@ async def get_pipedream_apps(
data = response.json()
print(data)
# print(data)
logger.info(f"Successfully fetched {len(data.get('data', []))} apps from Pipedream registry")
return {

View File

@ -15,7 +15,7 @@ import { Skeleton } from '@/components/ui/skeleton';
import { useRouter } from 'next/navigation';
// Import hooks and components from existing pages
import { useAgents, useUpdateAgent, useDeleteAgent, useOptimisticAgentUpdate, useCreateAgent } from '@/hooks/react-query/agents/use-agents';
import { useAgents, useUpdateAgent, useDeleteAgent, useOptimisticAgentUpdate, useCreateNewAgent } from '@/hooks/react-query/agents/use-agents';
import { useMarketplaceTemplates, useInstallTemplate, useMyTemplates, useUnpublishTemplate, usePublishTemplate } from '@/hooks/react-query/secure-mcp/use-secure-mcp';
import { useCreateCredentialProfile, type CreateCredentialProfileRequest } from '@/hooks/react-query/mcp/use-credential-profiles';
import { useMCPServerDetails } from '@/hooks/react-query/mcp/use-mcp-servers';
@ -108,13 +108,7 @@ interface MissingProfile {
required_config: string[];
}
interface AgentPreviewSheetProps {
item: MarketplaceTemplate | null;
open: boolean;
onOpenChange: (open: boolean) => void;
onInstall: (item: MarketplaceTemplate) => void;
isInstalling: boolean;
}
interface InstallDialogProps {
item: MarketplaceTemplate | null;
@ -124,158 +118,7 @@ interface InstallDialogProps {
isInstalling: boolean;
}
const AgentPreviewSheet: React.FC<AgentPreviewSheetProps> = ({
item,
open,
onOpenChange,
onInstall,
isInstalling
}) => {
if (!item) return null;
const { avatar, color } = item.avatar && item.avatar_color
? { avatar: item.avatar, color: item.avatar_color }
: getAgentAvatar(item.id);
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent>
<SheetHeader className="space-y-4">
<div className="flex items-start gap-4">
<div
className="h-16 w-16 flex items-center justify-center rounded-xl shrink-0"
style={{ backgroundColor: color }}
>
<div className="text-3xl">{avatar}</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<SheetTitle className="text-xl font-semibold line-clamp-2">
{item.name}
</SheetTitle>
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<User className="h-4 w-4" />
<span>{item.creator_name}</span>
</div>
<div className="flex items-center gap-1">
<Download className="h-4 w-4" />
<span>{item.download_count} downloads</span>
</div>
</div>
</div>
</div>
<Button
onClick={() => onInstall(item)}
disabled={isInstalling}
size='sm'
className='w-48'
>
{isInstalling ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Installing...
</>
) : (
<>
<Download className="h-4 w-4" />
Add to Library
</>
)}
</Button>
</SheetHeader>
<div className="px-4 space-y-6 py-6">
<div className="space-y-2">
<h3 className="font-medium text-xs text-muted-foreground uppercase tracking-wide">
Description
</h3>
<p className="text-sm leading-relaxed">
{item.description || 'No description available for this agent.'}
</p>
</div>
{item.tags && item.tags.length > 0 && (
<div className="space-y-2">
<h3 className="font-medium text-sm text-muted-foreground uppercase tracking-wide">
Tags
</h3>
<div className="flex flex-wrap gap-2">
{item.tags.map(tag => (
<Badge key={tag} variant="outline" className="text-xs">
<Tags className="h-3 w-3 mr-1" />
{tag}
</Badge>
))}
</div>
</div>
)}
{item.mcp_requirements && item.mcp_requirements.length > 0 && (
<div className="space-y-2">
<h3 className="font-medium text-xs text-muted-foreground uppercase tracking-wide">
Required Tools & MCPs
</h3>
<div className="space-y-2">
{item.mcp_requirements.map((mcp, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-muted-foreground/10 border rounded-lg">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10">
<Wrench className="h-4 w-4 text-primary" />
</div>
<div>
<div className="font-medium text-sm">{mcp.display_name}</div>
{mcp.enabled_tools && mcp.enabled_tools.length > 0 && (
<div className="text-xs text-muted-foreground">
{mcp.enabled_tools.length} tool{mcp.enabled_tools.length !== 1 ? 's' : ''}
</div>
)}
</div>
</div>
{mcp.custom_type && (
<Badge variant="outline" className="text-xs">
{mcp.custom_type.toUpperCase()}
</Badge>
)}
</div>
))}
</div>
</div>
)}
{item.metadata?.source_version_name && (
<div className="space-y-2">
<h3 className="font-medium text-sm text-muted-foreground uppercase tracking-wide">
Version
</h3>
<div className="flex items-center gap-2">
<GitBranch className="h-4 w-4 text-muted-foreground" />
<span className="text-sm">{item.metadata.source_version_name}</span>
</div>
</div>
)}
{item.marketplace_published_at && (
<div className="space-y-2">
<h3 className="font-medium text-xs text-muted-foreground uppercase tracking-wide">
Published
</h3>
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-muted-foreground" />
<span className="text-sm">{formatDate(item.marketplace_published_at)}</span>
</div>
</div>
)}
</div>
</SheetContent>
</Sheet>
);
};
const InstallDialog: React.FC<InstallDialogProps> = ({
item,
@ -939,7 +782,6 @@ export default function AgentsPage() {
const [installingItemId, setInstallingItemId] = useState<string | null>(null);
const [selectedItem, setSelectedItem] = useState<MarketplaceTemplate | null>(null);
const [showInstallDialog, setShowInstallDialog] = useState(false);
const [showPreviewSheet, setShowPreviewSheet] = useState(false);
// Templates state
const [templatesActioningId, setTemplatesActioningId] = useState<string | null>(null);
@ -987,7 +829,7 @@ export default function AgentsPage() {
const updateAgentMutation = useUpdateAgent();
const deleteAgentMutation = useDeleteAgent();
const createAgentMutation = useCreateAgent();
const createNewAgentMutation = useCreateNewAgent();
const { optimisticallyUpdateAgent, revertOptimisticUpdate } = useOptimisticAgentUpdate();
const installTemplateMutation = useInstallTemplate();
const unpublishMutation = useUnpublishTemplate();
@ -1138,44 +980,11 @@ export default function AgentsPage() {
setEditDialogOpen(true);
};
const handleCreateNewAgent = async () => {
try {
const { avatar, avatar_color } = generateRandomAvatar();
const defaultAgentData = {
name: 'New Agent',
description: 'A newly created agent',
system_prompt: 'You are a helpful assistant. Provide clear, accurate, and helpful responses to user queries.',
avatar,
avatar_color,
configured_mcps: [],
agentpress_tools: Object.fromEntries(
Object.entries(DEFAULT_AGENTPRESS_TOOLS).map(([key, value]) => [
key,
{ enabled: value.enabled, description: value.description }
])
),
is_default: false,
};
const newAgent = await createAgentMutation.mutateAsync(defaultAgentData);
router.push(`/agents/config/${newAgent.agent_id}`);
} catch (error) {
console.error('Error creating agent:', error);
}
const handleCreateNewAgent = () => {
createNewAgentMutation.mutate();
};
// Marketplace handlers
const handleItemClick = (item: MarketplaceTemplate) => {
setSelectedItem(item);
setShowPreviewSheet(true);
};
const handlePreviewInstall = (item: MarketplaceTemplate) => {
setShowPreviewSheet(false);
setShowInstallDialog(true);
};
const handleInstallClick = (item: MarketplaceTemplate, e?: React.MouseEvent) => {
if (e) {
e.stopPropagation();
@ -1288,13 +1097,6 @@ export default function AgentsPage() {
}
};
const handleTagFilter = (tag: string) => {
setMarketplaceSelectedTags(prev =>
prev.includes(tag)
? prev.filter(t => t !== tag)
: [...prev, tag]
);
};
const getItemStyling = (item: MarketplaceTemplate) => {
if (item.avatar && item.avatar_color) {
@ -1560,7 +1362,7 @@ export default function AgentsPage() {
<div
key={item.id}
className="bg-neutral-100 dark:bg-sidebar border border-border rounded-2xl overflow-hidden hover:bg-muted/50 transition-all duration-200 cursor-pointer group flex flex-col h-full"
onClick={() => handleItemClick(item)}
onClick={() => handleInstallClick(item)}
>
<div className="p-4">
<div className={`h-12 w-12 flex items-center justify-center rounded-lg`} style={{ backgroundColor: color }}>
@ -1658,7 +1460,7 @@ export default function AgentsPage() {
<div
key={item.id}
className="bg-neutral-100 dark:bg-sidebar border border-border rounded-2xl overflow-hidden hover:bg-muted/50 transition-all duration-200 cursor-pointer group flex flex-col h-full"
onClick={() => handleItemClick(item)}
onClick={() => handleInstallClick(item)}
>
<div className="p-4">
<div className={`h-12 w-12 flex items-center justify-center rounded-lg`} style={{ backgroundColor: color }}>
@ -1975,13 +1777,6 @@ export default function AgentsPage() {
</Dialog>
{/* Marketplace Dialogs */}
<AgentPreviewSheet
item={selectedItem}
open={showPreviewSheet}
onOpenChange={setShowPreviewSheet}
onInstall={handlePreviewInstall}
isInstalling={installingItemId === selectedItem?.id}
/>
<InstallDialog
item={selectedItem}
open={showInstallDialog}

View File

@ -10,6 +10,7 @@ import { AgentTriggersConfiguration } from './triggers/agent-triggers-configurat
import { AgentWorkflowsConfiguration } from './workflows/agent-workflows-configuration';
import { AgentKnowledgeBaseManager } from './knowledge-base/agent-knowledge-base-manager';
import { AgentToolsConfiguration } from './agent-tools-configuration';
import { AgentSelector } from '../thread/chat-input/agent-selector';
import { useAgent, useUpdateAgent } from '@/hooks/react-query/agents/use-agents';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
@ -42,9 +43,12 @@ export const AgentConfigModal: React.FC<AgentConfigModalProps> = ({
const updateAgentMutation = useUpdateAgent();
const router = useRouter();
const handleAgentSelect = (agentId: string | undefined) => {
onAgentSelect?.(agentId);
};
// Update active tab when initialTab changes or modal opens
React.useEffect(() => {
if (isOpen && initialTab) {
setActiveTab(initialTab);
}
}, [initialTab, isOpen]);
// Update local state when agent data changes
React.useEffect(() => {
@ -108,19 +112,10 @@ export const AgentConfigModal: React.FC<AgentConfigModalProps> = ({
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-5xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader className="flex-shrink-0 pb-2">
<div className="flex items-center justify-between">
<DialogTitle className="flex items-center gap-2">
<Settings2 className="h-5 w-5" />
Agent Configuration
</DialogTitle>
<Button
variant="ghost"
size="sm"
onClick={() => onOpenChange(false)}
>
<X className="h-4 w-4" />
</Button>
</div>
<DialogTitle className="flex items-center gap-2">
<Settings2 className="h-5 w-5" />
Agent Configuration
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-hidden flex flex-col">
@ -128,11 +123,10 @@ export const AgentConfigModal: React.FC<AgentConfigModalProps> = ({
<div className="flex-shrink-0 border-b pb-4 mb-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{/* <AgentSelector
selectedAgentId={selectedAgentId}
onAgentSelect={handleAgentSelect}
className="min-w-[250px]"
/> */}
<AgentSelector
selectedAgentId={selectedAgentId}
onAgentSelect={onAgentSelect}
/>
</div>
{selectedAgentId && (
<div className="flex items-center gap-2">

View File

@ -93,18 +93,30 @@ export const MCPConfigurationNew: React.FC<MCPConfigurationProps> = ({
)}
{configuredMCPs.length > 0 && (
<div className="bg-card rounded-xl border border-border overflow-hidden">
<div className="px-6 py-4 border-b border-border bg-muted/30">
<h4 className="text-sm font-medium text-foreground">
Configured Integrations
</h4>
<div className="space-y-4">
<div className="bg-card rounded-xl border border-border overflow-hidden">
<div className="px-6 py-4 border-b border-border bg-muted/30">
<h4 className="text-sm font-medium text-foreground">
Configured Integrations
</h4>
</div>
<div className="p-2 divide-y divide-border">
<ConfiguredMcpList
configuredMCPs={configuredMCPs}
onEdit={handleEditMCP}
onRemove={handleRemoveMCP}
/>
</div>
</div>
<div className="p-2 divide-y divide-border">
<ConfiguredMcpList
configuredMCPs={configuredMCPs}
onEdit={handleEditMCP}
onRemove={handleRemoveMCP}
/>
<div className="flex gap-2 justify-center">
<Button onClick={() => setShowRegistryDialog(true)} variant="default">
<Store className="h-4 w-4 mr-2" />
Browse Apps
</Button>
<Button onClick={() => setShowCustomDialog(true)} variant="outline">
<Server className="h-4 w-4 mr-2" />
Custom MCP
</Button>
</div>
</div>
)}

View File

@ -1,10 +1,4 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Plus, FileText, Loader2 } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useCreateAgent } from '@/hooks/react-query/agents/use-agents';
import { DEFAULT_AGENTPRESS_TOOLS } from './tools';
import { generateRandomAvatar } from '../../lib/utils/_avatar-generator';
interface ResultsInfoProps {
isLoading: boolean;
@ -21,35 +15,6 @@ export const ResultsInfo = ({
currentPage,
totalPages
}: ResultsInfoProps) => {
const router = useRouter();
const createAgentMutation = useCreateAgent();
const handleCreateNewAgent = async () => {
try {
const { avatar, avatar_color } = generateRandomAvatar();
const defaultAgentData = {
name: 'New Agent',
description: 'A newly created agent',
system_prompt: 'You are a helpful assistant. Provide clear, accurate, and helpful responses to user queries.',
avatar,
avatar_color,
configured_mcps: [],
agentpress_tools: Object.fromEntries(
Object.entries(DEFAULT_AGENTPRESS_TOOLS).map(([key, value]) => [
key,
{ enabled: value.enabled, description: value.description }
])
),
is_default: false,
};
const newAgent = await createAgentMutation.mutateAsync(defaultAgentData);
router.push(`/agents/config/${newAgent.agent_id}`);
} catch (error) {
console.error('Error creating agent:', error);
}
};
if (isLoading || totalAgents === 0) {
return null;

View File

@ -1,12 +1,12 @@
export const DEFAULT_AGENTPRESS_TOOLS: Record<string, { enabled: boolean; description: string; icon: string; color: string }> = {
'sb_shell_tool': { enabled: false, description: 'Execute shell commands in tmux sessions for terminal operations, CLI tools, and system management', icon: '💻', color: 'bg-slate-100 dark:bg-slate-800' },
'sb_files_tool': { enabled: false, description: 'Create, read, update, and delete files in the workspace with comprehensive file management', icon: '📁', color: 'bg-blue-100 dark:bg-blue-800/50' },
'sb_browser_tool': { enabled: false, description: 'Browser automation for web navigation, clicking, form filling, and page interaction', icon: '🌐', color: 'bg-indigo-100 dark:bg-indigo-800/50' },
'sb_deploy_tool': { enabled: false, description: 'Deploy applications and services with automated deployment capabilities', icon: '🚀', color: 'bg-green-100 dark:bg-green-800/50' },
'sb_expose_tool': { enabled: false, description: 'Expose services and manage ports for application accessibility', icon: '🔌', color: 'bg-orange-100 dark:bg-orange-800/20' },
'web_search_tool': { enabled: false, description: 'Search the web using Tavily API and scrape webpages with Firecrawl for research', icon: '🔍', color: 'bg-yellow-100 dark:bg-yellow-800/50' },
'sb_vision_tool': { enabled: false, description: 'Vision and image processing capabilities for visual content analysis', icon: '👁️', color: 'bg-pink-100 dark:bg-pink-800/50' },
'data_providers_tool': { enabled: false, description: 'Access to data providers and external APIs (requires RapidAPI key)', icon: '🔗', color: 'bg-cyan-100 dark:bg-cyan-800/50' },
'sb_shell_tool': { enabled: true, description: 'Execute shell commands in tmux sessions for terminal operations, CLI tools, and system management', icon: '💻', color: 'bg-slate-100 dark:bg-slate-800' },
'sb_files_tool': { enabled: true, description: 'Create, read, update, and delete files in the workspace with comprehensive file management', icon: '📁', color: 'bg-blue-100 dark:bg-blue-800/50' },
'sb_browser_tool': { enabled: true, description: 'Browser automation for web navigation, clicking, form filling, and page interaction', icon: '🌐', color: 'bg-indigo-100 dark:bg-indigo-800/50' },
'sb_deploy_tool': { enabled: true, description: 'Deploy applications and services with automated deployment capabilities', icon: '🚀', color: 'bg-green-100 dark:bg-green-800/50' },
'sb_expose_tool': { enabled: true, description: 'Expose services and manage ports for application accessibility', icon: '🔌', color: 'bg-orange-100 dark:bg-orange-800/20' },
'web_search_tool': { enabled: true, description: 'Search the web using Tavily API and scrape webpages with Firecrawl for research', icon: '🔍', color: 'bg-yellow-100 dark:bg-yellow-800/50' },
'sb_vision_tool': { enabled: true, description: 'Vision and image processing capabilities for visual content analysis', icon: '👁️', color: 'bg-pink-100 dark:bg-pink-800/50' },
'data_providers_tool': { enabled: true, description: 'Access to data providers and external APIs (requires RapidAPI key)', icon: '🔗', color: 'bg-cyan-100 dark:bg-cyan-800/50' },
};
export const getToolDisplayName = (toolName: string): string => {

View File

@ -17,8 +17,8 @@ import {
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { useAgents } from '@/hooks/react-query/agents/use-agents';
import { ChatSettingsDialog } from './chat-settings-dialog';
import { useAgents, useCreateNewAgent } from '@/hooks/react-query/agents/use-agents';
import { useRouter } from 'next/navigation';
import { cn, truncateString } from '@/lib/utils';
@ -47,38 +47,26 @@ const PREDEFINED_AGENTS: PredefinedAgent[] = [
// }
];
interface ChatSettingsDropdownProps {
interface AgentSelectorProps {
selectedAgentId?: string;
onAgentSelect?: (agentId: string | undefined) => void;
selectedModel: string;
onModelChange: (model: string) => void;
modelOptions: any[];
subscriptionStatus: any;
canAccessModel: (modelId: string) => boolean;
refreshCustomModels?: () => void;
disabled?: boolean;
}
export const ChatSettingsDropdown: React.FC<ChatSettingsDropdownProps> = ({
export const AgentSelector: React.FC<AgentSelectorProps> = ({
selectedAgentId,
onAgentSelect,
selectedModel,
onModelChange,
modelOptions,
subscriptionStatus,
canAccessModel,
refreshCustomModels,
disabled = 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();
const { data: agentsResponse, isLoading: agentsLoading } = useAgents();
const agents = agentsResponse?.agents || [];
const createNewAgentMutation = useCreateNewAgent();
// Combine all agents
const allAgents = [
@ -120,19 +108,31 @@ export const ChatSettingsDropdown: React.FC<ChatSettingsDropdownProps> = ({
const getAgentDisplay = () => {
const selectedAgent = allAgents.find(agent => agent.id === selectedAgentId);
if (selectedAgent) {
console.log('Selected agent found:', selectedAgent.name, 'with ID:', selectedAgent.id);
return {
name: selectedAgent.name,
icon: selectedAgent.icon
};
}
// If selectedAgentId is not undefined but no agent is found, log a warning
if (selectedAgentId !== undefined) {
console.warn('Agent with ID', selectedAgentId, 'not found, falling back to Suna');
}
// Default to Suna (the first agent which has id: undefined)
const defaultAgent = allAgents[0];
console.log('Using default agent:', defaultAgent.name);
return {
name: 'Suna',
icon: <Image src="/kortix-symbol.svg" alt="Suna" width={16} height={16} className="h-4 w-4 dark:invert" />
name: defaultAgent.name,
icon: defaultAgent.icon
};
};
const handleAgentSelect = (agentId: string | undefined) => {
console.log('Agent selected:', agentId === undefined ? 'Suna (default)' : agentId);
onAgentSelect?.(agentId);
setIsOpen(false);
};
@ -169,9 +169,9 @@ export const ChatSettingsDropdown: React.FC<ChatSettingsDropdownProps> = ({
router.push('/agents');
};
const handleMoreOptions = () => {
const handleCreateAgent = () => {
setIsOpen(false);
setDialogOpen(true);
createNewAgentMutation.mutate();
};
const renderAgentItem = (agent: any, index: number) => {
@ -325,42 +325,33 @@ export const ChatSettingsDropdown: React.FC<ChatSettingsDropdownProps> = ({
{/* Footer Actions */}
<div className="p-4 pt-3 border-t border-border/40">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center justify-center gap-3">
<Button
variant="outline"
variant="ghost"
size="sm"
onClick={handleExploreAll}
className="text-xs flex items-center gap-1.5 rounded-lg hover:bg-accent/30 transition-colors duration-200 border-border/50"
className="text-xs flex items-center gap-2 rounded-xl hover:bg-accent/40 transition-all duration-200 text-muted-foreground hover:text-foreground px-4 py-2"
>
<Search className="h-3 w-3" />
Explore All
<Search className="h-3.5 w-3.5" />
Explore All Agents
</Button>
<div className="w-px h-4 bg-border/60" />
<Button
variant="outline"
variant="ghost"
size="sm"
onClick={handleMoreOptions}
className="text-xs flex items-center gap-1.5 rounded-lg hover:bg-accent/30 transition-colors duration-200 border-border/50"
onClick={handleCreateAgent}
className="text-xs flex items-center gap-2 rounded-xl hover:bg-accent/40 transition-all duration-200 text-muted-foreground hover:text-foreground px-4 py-2"
>
<Settings className="h-3 w-3" />
More Options
<ChevronRight className="h-2.5 w-2.5" />
<Plus className="h-3.5 w-3.5" />
Create Agent
</Button>
</div>
</div>
</DropdownMenuContent>
</DropdownMenu>
<ChatSettingsDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
selectedModel={selectedModel}
onModelChange={onModelChange}
modelOptions={modelOptions}
subscriptionStatus={subscriptionStatus}
canAccessModel={canAccessModel}
refreshCustomModels={refreshCustomModels}
/>
</>
);
};

View File

@ -121,14 +121,54 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
const deleteFileMutation = useFileDelete();
const queryClient = useQueryClient();
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const fileInputRef = useRef<HTMLInputElement | null>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const hasLoadedFromLocalStorage = useRef(false);
useImperativeHandle(ref, () => ({
getPendingFiles: () => pendingFiles,
clearPendingFiles: () => setPendingFiles([]),
}));
// Load saved agent from localStorage on mount
useEffect(() => {
if (typeof window !== 'undefined' && onAgentSelect && !hasLoadedFromLocalStorage.current) {
// Don't load from localStorage if an agent is already selected
// or if there are URL parameters that might be setting the agent
const urlParams = new URLSearchParams(window.location.search);
const hasAgentIdInUrl = urlParams.has('agent_id');
if (!selectedAgentId && !hasAgentIdInUrl) {
const savedAgentId = localStorage.getItem('lastSelectedAgentId');
if (savedAgentId) {
// Convert 'suna' back to undefined for the default agent
const agentIdToSelect = savedAgentId === 'suna' ? undefined : savedAgentId;
console.log('Loading saved agent from localStorage:', savedAgentId);
onAgentSelect(agentIdToSelect);
} else {
console.log('No saved agent found in localStorage');
}
} else {
console.log('Skipping localStorage load:', {
hasSelectedAgent: !!selectedAgentId,
hasAgentIdInUrl,
selectedAgentId
});
}
hasLoadedFromLocalStorage.current = true;
}
}, [onAgentSelect, selectedAgentId]); // Keep selectedAgentId to check current state
// Save selected agent to localStorage whenever it changes
useEffect(() => {
if (typeof window !== 'undefined') {
// Use 'suna' as a special key for the default agent (undefined)
const keyToStore = selectedAgentId === undefined ? 'suna' : selectedAgentId;
console.log('Saving selected agent to localStorage:', keyToStore);
localStorage.setItem('lastSelectedAgentId', keyToStore);
}
}, [selectedAgentId]);
useEffect(() => {
if (autoFocus && textareaRef.current) {
textareaRef.current.focus();

View File

@ -1,110 +0,0 @@
'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';
import { BillingModal } from '@/components/billing/billing-modal';
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 [billingModalOpen, setBillingModalOpen] = 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}
billingModalOpen={billingModalOpen}
setBillingModalOpen={setBillingModalOpen}
/>
</div>
{/* Billing Modal */}
<BillingModal
open={billingModalOpen}
onOpenChange={setBillingModalOpen}
returnUrl={typeof window !== 'undefined' ? window.location.href : '/'}
/>
<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

@ -7,7 +7,7 @@ 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 { AgentSelector } from './agent-selector';
import { canAccessModel, SubscriptionStatus } from './_use-model-selection';
import { isLocalMode } from '@/lib/config';
import { useFeatureFlag } from '@/lib/feature-flags';
@ -131,41 +131,29 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
const renderDropdown = () => {
if (isLoggedIn) {
if (hideAgentSelection) {
return <ModelSelector
selectedModel={selectedModel}
onModelChange={onModelChange}
modelOptions={modelOptions}
subscriptionStatus={subscriptionStatus}
canAccessModel={canAccessModel}
refreshCustomModels={refreshCustomModels}
billingModalOpen={billingModalOpen}
setBillingModalOpen={setBillingModalOpen}
/>
} else if (enableAdvancedConfig || (customAgentsEnabled && !flagsLoading)) {
return <ChatSettingsDropdown
selectedAgentId={selectedAgentId}
onAgentSelect={onAgentSelect}
selectedModel={selectedModel}
onModelChange={onModelChange}
modelOptions={modelOptions}
subscriptionStatus={subscriptionStatus}
canAccessModel={canAccessModel}
refreshCustomModels={refreshCustomModels}
disabled={loading || (disabled && !isAgentRunning)}
/>
} else {
return <ModelSelector
selectedModel={selectedModel}
onModelChange={onModelChange}
modelOptions={modelOptions}
subscriptionStatus={subscriptionStatus}
canAccessModel={canAccessModel}
refreshCustomModels={refreshCustomModels}
billingModalOpen={billingModalOpen}
setBillingModalOpen={setBillingModalOpen}
/>
}
const showAdvancedFeatures = enableAdvancedConfig || (customAgentsEnabled && !flagsLoading);
return (
<div className="flex items-center gap-2">
{showAdvancedFeatures && !hideAgentSelection && (
<AgentSelector
selectedAgentId={selectedAgentId}
onAgentSelect={onAgentSelect}
disabled={loading || (disabled && !isAgentRunning)}
/>
)}
<ModelSelector
selectedModel={selectedModel}
onModelChange={onModelChange}
modelOptions={modelOptions}
subscriptionStatus={subscriptionStatus}
canAccessModel={canAccessModel}
refreshCustomModels={refreshCustomModels}
billingModalOpen={billingModalOpen}
setBillingModalOpen={setBillingModalOpen}
/>
</div>
);
}
return <ChatDropdown />;
}

View File

@ -14,7 +14,7 @@ import {
TooltipTrigger,
} from '@/components/ui/tooltip';
import { Button } from '@/components/ui/button';
import { Check, ChevronDown, Search, AlertTriangle, Crown, ArrowUpRight, Brain, Plus, Edit, Trash } from 'lucide-react';
import { Check, ChevronDown, Search, AlertTriangle, Crown, ArrowUpRight, Brain, Plus, Edit, Trash, Cpu } from 'lucide-react';
import {
ModelOption,
SubscriptionStatus,
@ -91,9 +91,6 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
}
}, [customModels]);
// Get current custom models from state
const currentCustomModels = customModels || [];
// Enhance model options with capabilities - using a Map to ensure uniqueness
const modelMap = new Map();
@ -511,30 +508,29 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
return (
<div className="relative">
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<Button
variant={hasBorder ? "outline" : "ghost"}
size="default"
className="h-8 rounded-lg text-muted-foreground shadow-none border-none focus:ring-0 px-3"
>
<div className="flex items-center gap-1 text-sm font-medium">
{MODELS[selectedModel]?.lowQuality && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<AlertTriangle className="h-3.5 w-3.5 text-amber-500 mr-1" />
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
<p>Basic model with limited capabilities</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<span className="truncate max-w-[100px] sm:max-w-[160px] md:max-w-[200px] lg:max-w-none">{selectedLabel}</span>
<ChevronDown className="h-3 w-3 opacity-50 ml-1 flex-shrink-0" />
</div>
</Button>
</DropdownMenuTrigger>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 px-2 py-2 bg-transparent border-0 rounded-xl text-muted-foreground hover:text-foreground hover:bg-accent/50 flex items-center gap-2"
>
<div className="relative flex items-center justify-center">
<Cpu className="h-4 w-4" />
{MODELS[selectedModel]?.lowQuality && (
<AlertTriangle className="h-2.5 w-2.5 text-amber-500 absolute -top-1 -right-1" />
)}
</div>
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
<p>Choose a model</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<DropdownMenuContent
align="end"

View File

@ -1,6 +1,12 @@
import React, { useState, useRef, useEffect } from 'react';
import { Mic, Square, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { useTranscription } from '@/hooks/react-query/transcription/use-transcription';
interface VoiceRecorderProps {
@ -136,11 +142,11 @@ export const VoiceRecorder: React.FC<VoiceRecorderProps> = ({
const getButtonClass = () => {
switch (state) {
case 'recording':
return 'text-red-500 hover:bg-red-600';
return 'text-red-500 hover:bg-red-50 hover:text-red-600';
case 'processing':
return 'hover:bg-gray-100';
return '';
default:
return 'hover:bg-gray-100';
return '';
}
};
@ -156,17 +162,32 @@ export const VoiceRecorder: React.FC<VoiceRecorderProps> = ({
};
return (
<Button
type="button"
variant="ghost"
size="sm"
onClick={handleClick}
onContextMenu={handleRightClick}
disabled={disabled || state === 'processing'}
className={`h-8 w-8 p-0 transition-colors ${getButtonClass()}`}
title={state === 'recording' ? 'Click to stop' : 'Click to start recording'}
>
{getIcon()}
</Button>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="sm"
onClick={handleClick}
onContextMenu={handleRightClick}
disabled={disabled || state === 'processing'}
className={`h-8 px-2 py-2 bg-transparent border-0 rounded-xl text-muted-foreground hover:text-foreground hover:bg-accent/50 flex items-center gap-2 transition-colors ${getButtonClass()}`}
>
{getIcon()}
</Button>
</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
<p>
{state === 'recording'
? 'Click to stop recording'
: state === 'processing'
? 'Processing...'
: 'Record voice message'
}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};

View File

@ -4,6 +4,9 @@ import { toast } from 'sonner';
import { agentKeys } from './keys';
import { Agent, AgentUpdateRequest, AgentsParams, createAgent, deleteAgent, getAgent, getAgents, getThreadAgent, updateAgent, AgentBuilderChatRequest, AgentBuilderStreamData, startAgentBuilderChat, getAgentBuilderChatHistory } from './utils';
import { useRef, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { generateRandomAvatar } from '@/lib/utils/_avatar-generator';
import { DEFAULT_AGENTPRESS_TOOLS } from '@/components/agents/tools';
export const useAgents = (params: AgentsParams = {}) => {
return createQueryHook(
@ -44,6 +47,45 @@ export const useCreateAgent = () => {
)();
};
export const useCreateNewAgent = () => {
const router = useRouter();
const createAgentMutation = useCreateAgent();
return createMutationHook(
async (_: void) => {
const { avatar, avatar_color } = generateRandomAvatar();
const defaultAgentData = {
name: 'New Agent',
description: '',
system_prompt: 'You are a helpful assistant. Provide clear, accurate, and helpful responses to user queries.',
avatar,
avatar_color,
configured_mcps: [],
agentpress_tools: Object.fromEntries(
Object.entries(DEFAULT_AGENTPRESS_TOOLS).map(([key, value]) => [
key,
{ enabled: value.enabled, description: value.description }
])
),
is_default: false,
};
const newAgent = await createAgentMutation.mutateAsync(defaultAgentData);
return newAgent;
},
{
onSuccess: (newAgent) => {
router.push(`/agents/config/${newAgent.agent_id}`);
},
onError: (error) => {
console.error('Error creating agent:', error);
toast.error('Failed to create agent. Please try again.');
},
}
)();
};
export const useUpdateAgent = () => {
const queryClient = useQueryClient();