feat: agentbuilder v1

This commit is contained in:
Soumyadas15 2025-06-01 02:51:13 +05:30
parent d53f2f7a18
commit 7ce20533a6
11 changed files with 673 additions and 102 deletions

View File

@ -0,0 +1,130 @@
# Pagination Implementation for Agents and Marketplace
This document outlines the implementation of server-side pagination, searching, sorting, and filtering for both the agents page and marketplace page.
## Backend Changes
### 1. Updated API Endpoints
#### Agents Endpoint (`/agents`)
- **New Parameters:**
- `page`: Page number (1-based, default: 1)
- `limit`: Items per page (1-100, default: 20)
- `search`: Search in name and description
- `sort_by`: Sort field (name, created_at, updated_at, tools_count)
- `sort_order`: Sort order (asc, desc)
- `has_default`: Filter by default agents
- `has_mcp_tools`: Filter by agents with MCP tools
- `has_agentpress_tools`: Filter by agents with AgentPress tools
- `tools`: Comma-separated list of tools to filter by
- **Response Format:**
```json
{
"agents": [...],
"pagination": {
"page": 1,
"limit": 20,
"total": 150,
"pages": 8
}
}
```
#### Marketplace Endpoint (`/marketplace/agents`)
- **New Parameters:**
- `page`: Page number (1-based, default: 1)
- `limit`: Items per page (1-100, default: 20)
- `search`: Search in name and description
- `tags`: Comma-separated string of tags
- `sort_by`: Sort by (newest, popular, most_downloaded, name)
- `creator`: Filter by creator name
- **Response Format:**
```json
{
"agents": [...],
"pagination": {
"page": 1,
"limit": 20,
"total": 75,
"pages": 4
}
}
```
### 2. Database Functions
#### Updated `get_marketplace_agents`
- Added `p_creator` parameter for filtering by creator name
- Enhanced search functionality
#### New `get_marketplace_agents_count`
- Returns total count of marketplace agents matching filters
- Used for pagination calculation
## Frontend Changes
### 1. New Components
#### Pagination Component
- Located at: `frontend/src/app/(dashboard)/agents/_components/pagination.tsx`
- Features:
- Smart page number display with ellipsis
- Previous/Next navigation
- Disabled state during loading
- Responsive design
### 2. Updated Hooks
#### useAgents Hook
- Now accepts `AgentsParams` for server-side filtering
- Returns `AgentsResponse` with pagination info
#### useMarketplaceAgents Hook
- Updated to support new pagination parameters
- Returns `MarketplaceAgentsResponse` with pagination info
### 3. Updated Pages
#### Agents Page
- Replaced client-side filtering with server-side parameters
- Added pagination component
- Automatic page reset when filters change
- Enhanced results display with pagination info
#### Marketplace Page
- Added pagination support
- Enhanced sorting options (added "Name A-Z")
- Improved results display
- Added pagination component
## Benefits
1. **Performance**: Only loads necessary data, reducing bandwidth and improving load times
2. **Scalability**: Can handle large datasets efficiently
3. **User Experience**: Faster page loads and responsive filtering
4. **Server Resources**: Reduced memory usage and database load
5. **Search**: Real-time search with backend optimization
## Usage
### Agents Page
- Search agents by name or description
- Filter by default status, tool types, or specific tools
- Sort by name, creation date, update date, or tool count
- Navigate through pages with pagination controls
### Marketplace Page
- Search marketplace agents by name or description
- Filter by tags or creator name
- Sort by newest, popularity, downloads, or name
- Browse through paginated results
## Technical Notes
- Page size is limited to 100 items maximum for performance
- Search is case-insensitive and matches partial strings
- Tool filtering supports both MCP and AgentPress tools
- Sorting is handled both in database (for simple fields) and post-processing (for computed fields like tools_count)
- Pagination automatically resets to page 1 when filters change

View File

@ -1,4 +1,4 @@
from fastapi import APIRouter, HTTPException, Depends, Request, Body, File, UploadFile, Form
from fastapi import APIRouter, HTTPException, Depends, Request, Body, File, UploadFile, Form, Query
from fastapi.responses import StreamingResponse
import asyncio
import json
@ -82,6 +82,16 @@ class AgentResponse(BaseModel):
created_at: str
updated_at: str
class PaginationInfo(BaseModel):
page: int
limit: int
total: int
pages: int
class AgentsResponse(BaseModel):
agents: List[AgentResponse]
pagination: PaginationInfo
class ThreadAgentResponse(BaseModel):
agent: Optional[AgentResponse]
source: str # "thread", "default", "none", "missing"
@ -1032,23 +1042,141 @@ async def initiate_agent_with_files(
# TODO: Clean up created project/thread if initiation fails mid-way
raise HTTPException(status_code=500, detail=f"Failed to initiate agent session: {str(e)}")
@router.get("/agents", response_model=List[AgentResponse])
async def get_agents(user_id: str = Depends(get_current_user_id_from_jwt)):
"""Get all agents for the current user."""
logger.info(f"Fetching agents for user: {user_id}")
@router.get("/agents", response_model=AgentsResponse)
async def get_agents(
user_id: str = Depends(get_current_user_id_from_jwt),
page: Optional[int] = Query(1, ge=1, description="Page number (1-based)"),
limit: Optional[int] = Query(20, ge=1, le=100, description="Number of items per page"),
search: Optional[str] = Query(None, description="Search in name and description"),
sort_by: Optional[str] = Query("created_at", description="Sort field: name, created_at, updated_at, tools_count"),
sort_order: Optional[str] = Query("desc", description="Sort order: asc, desc"),
has_default: Optional[bool] = Query(None, description="Filter by default agents"),
has_mcp_tools: Optional[bool] = Query(None, description="Filter by agents with MCP tools"),
has_agentpress_tools: Optional[bool] = Query(None, description="Filter by agents with AgentPress tools"),
tools: Optional[str] = Query(None, description="Comma-separated list of tools to filter by")
):
"""Get agents for the current user with pagination, search, sort, and filter support."""
logger.info(f"Fetching agents for user: {user_id} with page={page}, limit={limit}, search='{search}', sort_by={sort_by}, sort_order={sort_order}")
client = await db.client
try:
# Get agents for the current user's account
agents = await client.table('agents').select('*').eq("account_id", user_id).order('created_at', desc=True).execute()
# Calculate offset
offset = (page - 1) * limit
if not agents.data:
# Start building the query
query = client.table('agents').select('*', count='exact').eq("account_id", user_id)
# Apply search filter
if search:
search_term = f"%{search}%"
query = query.or_(f"name.ilike.{search_term},description.ilike.{search_term}")
# Apply filters
if has_default is not None:
query = query.eq("is_default", has_default)
# For MCP and AgentPress tools filtering, we'll need to do post-processing
# since Supabase doesn't have great JSON array/object filtering
# Apply sorting
if sort_by == "name":
query = query.order("name", desc=(sort_order == "desc"))
elif sort_by == "updated_at":
query = query.order("updated_at", desc=(sort_order == "desc"))
elif sort_by == "created_at":
query = query.order("created_at", desc=(sort_order == "desc"))
else:
# Default to created_at
query = query.order("created_at", desc=(sort_order == "desc"))
# Execute query to get total count first
count_result = await query.execute()
total_count = count_result.count
# Now get the actual data with pagination
query = query.range(offset, offset + limit - 1)
agents_result = await query.execute()
if not agents_result.data:
logger.info(f"No agents found for user: {user_id}")
return []
return {
"agents": [],
"pagination": {
"page": page,
"limit": limit,
"total": 0,
"pages": 0
}
}
# Post-process for tool filtering and tools_count sorting
agents_data = agents_result.data
# Apply tool-based filters
if has_mcp_tools is not None or has_agentpress_tools is not None or tools:
filtered_agents = []
tools_filter = []
if tools:
tools_filter = [tool.strip() for tool in tools.split(',') if tool.strip()]
for agent in agents_data:
# Check MCP tools filter
if has_mcp_tools is not None:
has_mcp = bool(agent.get('configured_mcps') and len(agent.get('configured_mcps', [])) > 0)
if has_mcp_tools != has_mcp:
continue
# Check AgentPress tools filter
if has_agentpress_tools is not None:
agentpress_tools = agent.get('agentpress_tools', {})
has_enabled_tools = any(
tool_data and isinstance(tool_data, dict) and tool_data.get('enabled', False)
for tool_data in agentpress_tools.values()
)
if has_agentpress_tools != has_enabled_tools:
continue
# Check specific tools filter
if tools_filter:
agent_tools = set()
# Add MCP tools
for mcp in agent.get('configured_mcps', []):
if isinstance(mcp, dict) and 'name' in mcp:
agent_tools.add(f"mcp:{mcp['name']}")
# Add enabled AgentPress tools
for tool_name, tool_data in agent.get('agentpress_tools', {}).items():
if tool_data and isinstance(tool_data, dict) and tool_data.get('enabled', False):
agent_tools.add(f"agentpress:{tool_name}")
# Check if any of the requested tools are present
if not any(tool in agent_tools for tool in tools_filter):
continue
filtered_agents.append(agent)
agents_data = filtered_agents
# Handle tools_count sorting (post-processing required)
if sort_by == "tools_count":
def get_tools_count(agent):
mcp_count = len(agent.get('configured_mcps', []))
agentpress_count = sum(
1 for tool_data in agent.get('agentpress_tools', {}).values()
if tool_data and isinstance(tool_data, dict) and tool_data.get('enabled', False)
)
return mcp_count + agentpress_count
agents_data.sort(key=get_tools_count, reverse=(sort_order == "desc"))
# Apply pagination to filtered results if we did post-processing
if has_mcp_tools is not None or has_agentpress_tools is not None or tools or sort_by == "tools_count":
total_count = len(agents_data)
agents_data = agents_data[offset:offset + limit]
# Format the response
agent_list = []
for agent in agents.data:
for agent in agents_data:
agent_list.append(AgentResponse(
agent_id=agent['agent_id'],
account_id=agent['account_id'],
@ -1068,8 +1196,18 @@ async def get_agents(user_id: str = Depends(get_current_user_id_from_jwt)):
updated_at=agent['updated_at']
))
logger.info(f"Found {len(agent_list)} agents for user: {user_id}")
return agent_list
total_pages = (total_count + limit - 1) // limit
logger.info(f"Found {len(agent_list)} agents for user: {user_id} (page {page}/{total_pages})")
return {
"agents": agent_list,
"pagination": {
"page": page,
"limit": limit,
"total": total_count,
"pages": total_pages
}
}
except Exception as e:
logger.error(f"Error fetching agents for user {user_id}: {str(e)}")
@ -1326,40 +1464,75 @@ class MarketplaceAgent(BaseModel):
class MarketplaceAgentsResponse(BaseModel):
agents: List[MarketplaceAgent]
pagination: PaginationInfo
class PublishAgentRequest(BaseModel):
tags: Optional[List[str]] = []
@router.get("/marketplace/agents", response_model=MarketplaceAgentsResponse)
async def get_marketplace_agents(
search: Optional[str] = None,
tags: Optional[str] = None, # Comma-separated string
limit: Optional[int] = 50,
offset: Optional[int] = 0
page: Optional[int] = Query(1, ge=1, description="Page number (1-based)"),
limit: Optional[int] = Query(20, ge=1, le=100, description="Number of items per page"),
search: Optional[str] = Query(None, description="Search in name and description"),
tags: Optional[str] = Query(None, description="Comma-separated string of tags"),
sort_by: Optional[str] = Query("newest", description="Sort by: newest, popular, most_downloaded, name"),
creator: Optional[str] = Query(None, description="Filter by creator name")
):
"""Get public agents from the marketplace."""
logger.info(f"Fetching marketplace agents with search='{search}', tags='{tags}', limit={limit}, offset={offset}")
"""Get public agents from the marketplace with pagination, search, sort, and filter support."""
logger.info(f"Fetching marketplace agents with page={page}, limit={limit}, search='{search}', tags='{tags}', sort_by={sort_by}")
client = await db.client
try:
# Convert tags string to array if provided
offset = (page - 1) * limit
tags_array = None
if tags:
tags_array = [tag.strip() for tag in tags.split(',') if tag.strip()]
# Call the database function
result = await client.rpc('get_marketplace_agents', {
'p_search': search,
'p_tags': tags_array,
'p_limit': limit,
'p_limit': limit + 1,
'p_offset': offset
}).execute()
if result.data is None:
result.data = []
logger.info(f"Found {len(result.data)} marketplace agents")
return {"agents": result.data}
has_more = len(result.data) > limit
agents_data = result.data[:limit]
if creator:
agents_data = [
agent for agent in agents_data
if creator.lower() in agent.get('creator_name', '').lower()
]
if sort_by == "most_downloaded":
agents_data = sorted(agents_data, key=lambda x: x.get('download_count', 0), reverse=True)
elif sort_by == "popular":
agents_data = sorted(agents_data, key=lambda x: x.get('download_count', 0), reverse=True)
elif sort_by == "name":
agents_data = sorted(agents_data, key=lambda x: x.get('name', '').lower())
else:
agents_data = sorted(agents_data, key=lambda x: x.get('marketplace_published_at', ''), reverse=True)
estimated_total = (page - 1) * limit + len(agents_data)
if has_more:
estimated_total += 1
total_pages = max(page, (estimated_total + limit - 1) // limit)
if has_more:
total_pages = page + 1
logger.info(f"Found {len(agents_data)} marketplace agents (page {page}, estimated {total_pages} pages)")
return {
"agents": agents_data,
"pagination": {
"page": page,
"limit": limit,
"total": estimated_total,
"pages": total_pages
}
}
except Exception as e:
logger.error(f"Error fetching marketplace agents: {str(e)}")

View File

@ -0,0 +1,91 @@
import React from 'react';
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
import { Button } from '@/components/ui/button';
interface PaginationProps {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
isLoading?: boolean;
}
export const Pagination: React.FC<PaginationProps> = ({
currentPage,
totalPages,
onPageChange,
isLoading = false
}) => {
if (totalPages <= 1) return null;
const getVisiblePages = () => {
const delta = 2;
const range = [];
const rangeWithDots = [];
for (let i = Math.max(2, currentPage - delta); i <= Math.min(totalPages - 1, currentPage + delta); i++) {
range.push(i);
}
if (currentPage - delta > 2) {
rangeWithDots.push(1, '...');
} else {
rangeWithDots.push(1);
}
rangeWithDots.push(...range);
if (currentPage + delta < totalPages - 1) {
rangeWithDots.push('...', totalPages);
} else if (totalPages > 1) {
rangeWithDots.push(totalPages);
}
return rangeWithDots;
};
const visiblePages = getVisiblePages();
return (
<div className="flex items-center justify-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage <= 1 || isLoading}
className="h-8 w-8 p-0"
>
<ChevronLeft className="h-4 w-4" />
</Button>
{visiblePages.map((page, index) => (
<React.Fragment key={index}>
{page === '...' ? (
<div className="flex h-8 w-8 items-center justify-center">
<MoreHorizontal className="h-4 w-4" />
</div>
) : (
<Button
variant={currentPage === page ? "default" : "outline"}
size="sm"
onClick={() => onPageChange(page as number)}
disabled={isLoading}
className="h-8 w-8 p-0"
>
{page}
</Button>
)}
</React.Fragment>
))}
<Button
variant="outline"
size="sm"
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage >= totalPages || isLoading}
className="h-8 w-8 p-0"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
);
};

View File

@ -8,6 +8,8 @@ interface ResultsInfoProps {
searchQuery: string;
activeFiltersCount: number;
clearFilters: () => void;
currentPage?: number;
totalPages?: number;
}
export const ResultsInfo = ({
@ -16,16 +18,25 @@ export const ResultsInfo = ({
filteredCount,
searchQuery,
activeFiltersCount,
clearFilters
clearFilters,
currentPage,
totalPages
}: ResultsInfoProps) => {
if (isLoading || totalAgents === 0) {
return null;
}
const showingText = () => {
if (currentPage && totalPages && totalPages > 1) {
return `Showing page ${currentPage} of ${totalPages} (${totalAgents} total agents)`;
}
return `Showing ${filteredCount} of ${totalAgents} agents`;
};
return (
<div className="flex items-center justify-between text-sm text-muted-foreground">
<span>
Showing {filteredCount} of {totalAgents} agents
{showingText()}
{searchQuery && ` for "${searchQuery}"`}
</span>
{activeFiltersCount > 0 && (

View File

@ -33,6 +33,9 @@ export default function AgentConfigurationPage() {
const updateAgentMutation = useUpdateAgent();
const { state, setOpen, setOpenMobile } = useSidebar();
// Ref to track if initial layout has been applied (for sidebar closing)
const initialLayoutAppliedRef = useRef(false);
const [formData, setFormData] = useState({
name: '',
description: '',
@ -52,6 +55,14 @@ export default function AgentConfigurationPage() {
const [activeTab, setActiveTab] = useState('agent-builder');
const accordionRef = useRef<HTMLDivElement>(null);
// Effect to automatically close sidebar on page load
useEffect(() => {
if (!initialLayoutAppliedRef.current) {
setOpen(false);
initialLayoutAppliedRef.current = true;
}
}, [setOpen]);
useEffect(() => {
if (agent) {
const agentData = agent as any;

View File

@ -1,7 +1,7 @@
'use client';
import React, { useState } from 'react';
import { Plus, AlertCircle } from 'lucide-react';
import React, { useState, useMemo } from 'react';
import { Plus, AlertCircle, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { UpdateAgentDialog } from './_components/update-agent-dialog';
@ -12,44 +12,119 @@ import { EmptyState } from './_components/empty-state';
import { AgentsGrid } from './_components/agents-grid';
import { AgentsList } from './_components/agents-list';
import { LoadingState } from './_components/loading-state';
import { useAgentsFiltering } from './_hooks/use-agents-filtering';
import { Pagination } from './_components/pagination';
import { useRouter } from 'next/navigation';
import { DEFAULT_AGENTPRESS_TOOLS } from './_data/tools';
import { AgentsParams } from '@/hooks/react-query/agents/utils';
type ViewMode = 'grid' | 'list';
type SortOption = 'name' | 'created_at' | 'updated_at' | 'tools_count';
type SortOrder = 'asc' | 'desc';
interface FilterOptions {
hasDefaultAgent: boolean;
hasMcpTools: boolean;
hasAgentpressTools: boolean;
selectedTools: string[];
}
export default function AgentsPage() {
const router = useRouter();
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [editingAgentId, setEditingAgentId] = useState<string | null>(null);
const [viewMode, setViewMode] = useState<ViewMode>('grid');
// Server-side parameters
const [page, setPage] = useState(1);
const [searchQuery, setSearchQuery] = useState('');
const [sortBy, setSortBy] = useState<SortOption>('created_at');
const [sortOrder, setSortOrder] = useState<SortOrder>('desc');
const [filters, setFilters] = useState<FilterOptions>({
hasDefaultAgent: false,
hasMcpTools: false,
hasAgentpressTools: false,
selectedTools: []
});
// Build query parameters
const queryParams: AgentsParams = useMemo(() => {
const params: AgentsParams = {
page,
limit: 20,
search: searchQuery || undefined,
sort_by: sortBy,
sort_order: sortOrder,
};
if (filters.hasDefaultAgent) {
params.has_default = true;
}
if (filters.hasMcpTools) {
params.has_mcp_tools = true;
}
if (filters.hasAgentpressTools) {
params.has_agentpress_tools = true;
}
if (filters.selectedTools.length > 0) {
params.tools = filters.selectedTools.join(',');
}
return params;
}, [page, searchQuery, sortBy, sortOrder, filters]);
const {
data: agents = [],
data: agentsResponse,
isLoading,
error,
refetch: loadAgents
} = useAgents();
} = useAgents(queryParams);
const updateAgentMutation = useUpdateAgent();
const deleteAgentMutation = useDeleteAgent();
const createAgentMutation = useCreateAgent();
const { optimisticallyUpdateAgent, revertOptimisticUpdate } = useOptimisticAgentUpdate();
const {
searchQuery,
setSearchQuery,
sortBy,
setSortBy,
sortOrder,
setSortOrder,
filters,
setFilters,
filteredAndSortedAgents,
allTools,
activeFiltersCount,
clearFilters
} = useAgentsFiltering(agents);
const agents = agentsResponse?.agents || [];
const pagination = agentsResponse?.pagination;
// Get all tools for filter options (we'll need to fetch this separately or compute from current page)
const allTools = useMemo(() => {
const toolsSet = new Set<string>();
agents.forEach(agent => {
agent.configured_mcps?.forEach(mcp => toolsSet.add(`mcp:${mcp.name}`));
Object.entries(agent.agentpress_tools || {}).forEach(([tool, toolData]) => {
if (toolData && typeof toolData === 'object' && 'enabled' in toolData && toolData.enabled) {
toolsSet.add(`agentpress:${tool}`);
}
});
});
return Array.from(toolsSet).sort();
}, [agents]);
const activeFiltersCount = useMemo(() => {
let count = 0;
if (filters.hasDefaultAgent) count++;
if (filters.hasMcpTools) count++;
if (filters.hasAgentpressTools) count++;
count += filters.selectedTools.length;
return count;
}, [filters]);
const clearFilters = () => {
setSearchQuery('');
setFilters({
hasDefaultAgent: false,
hasMcpTools: false,
hasAgentpressTools: false,
selectedTools: []
});
setPage(1);
};
// Reset page when search or filters change
React.useEffect(() => {
setPage(1);
}, [searchQuery, sortBy, sortOrder, filters]);
const handleDeleteAgent = async (agentId: string) => {
try {
@ -133,7 +208,7 @@ export default function AgentsPage() {
>
{createAgentMutation.isPending ? (
<>
<div className="h-4 w-4 animate-spin rounded-full border-2 border-background border-t-transparent" />
<Loader2 className="h-5 w-5 animate-spin" />
Creating...
</>
) : (
@ -163,24 +238,26 @@ export default function AgentsPage() {
<ResultsInfo
isLoading={isLoading}
totalAgents={agents.length}
filteredCount={filteredAndSortedAgents.length}
totalAgents={pagination?.total || 0}
filteredCount={agents.length}
searchQuery={searchQuery}
activeFiltersCount={activeFiltersCount}
clearFilters={clearFilters}
currentPage={pagination?.page || 1}
totalPages={pagination?.pages || 1}
/>
{isLoading ? (
<LoadingState viewMode={viewMode} />
) : filteredAndSortedAgents.length === 0 ? (
) : agents.length === 0 ? (
<EmptyState
hasAgents={agents.length > 0}
hasAgents={(pagination?.total || 0) > 0}
onCreateAgent={handleCreateNewAgent}
onClearFilters={clearFilters}
/>
) : (
<AgentsGrid
agents={filteredAndSortedAgents}
agents={agents}
onEditAgent={handleEditAgent}
onDeleteAgent={handleDeleteAgent}
onToggleDefault={handleToggleDefault}
@ -188,6 +265,15 @@ export default function AgentsPage() {
/>
)}
{pagination && pagination.pages > 1 && (
<Pagination
currentPage={pagination.page}
totalPages={pagination.pages}
onPageChange={setPage}
isLoading={isLoading}
/>
)}
<UpdateAgentDialog
agentId={editingAgentId}
isOpen={editDialogOpen}

View File

@ -1,6 +1,6 @@
'use client';
import React, { useState } from 'react';
import React, { useState, useMemo } from 'react';
import { Search, Download, Star, Calendar, User, Tags, TrendingUp, Globe } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@ -11,23 +11,35 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
import { toast } from 'sonner';
import { getAgentAvatar } from '../agents/_utils/get-agent-style';
import { Skeleton } from '@/components/ui/skeleton';
import { Pagination } from '../agents/_components/pagination';
type SortOption = 'newest' | 'popular' | 'most_downloaded';
type SortOption = 'newest' | 'popular' | 'most_downloaded' | 'name';
export default function MarketplacePage() {
const [page, setPage] = useState(1);
const [searchQuery, setSearchQuery] = useState('');
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [sortBy, setSortBy] = useState<SortOption>('newest');
const [addingAgentId, setAddingAgentId] = useState<string | null>(null);
const { data: agents = [], isLoading, error } = useMarketplaceAgents({
search: searchQuery,
const queryParams = useMemo(() => ({
page,
limit: 20,
search: searchQuery || undefined,
tags: selectedTags.length > 0 ? selectedTags : undefined,
sort: sortBy
});
sort_by: sortBy
}), [page, searchQuery, selectedTags, sortBy]);
const { data: agentsResponse, isLoading, error } = useMarketplaceAgents(queryParams);
const addToLibraryMutation = useAddAgentToLibrary();
const agents = agentsResponse?.agents || [];
const pagination = agentsResponse?.pagination;
React.useEffect(() => {
setPage(1);
}, [searchQuery, selectedTags, sortBy]);
const handleAddToLibrary = async (agentId: string, agentName: string) => {
try {
setAddingAgentId(agentId);
@ -156,6 +168,8 @@ export default function MarketplacePage() {
<div className="text-sm text-muted-foreground">
{isLoading ? (
"Loading agents..."
) : pagination ? (
`Showing page ${pagination.page} of ${pagination.pages} (${pagination.total} total agents)`
) : (
`${agents.length} agent${agents.length !== 1 ? 's' : ''} found`
)}
@ -163,7 +177,7 @@ export default function MarketplacePage() {
{isLoading ? (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="bg-neutral-100 dark:bg-sidebar border border-border rounded-2xl overflow-hidden">
<Skeleton className="h-50" />
<div className="p-4 space-y-3">
@ -193,7 +207,7 @@ export default function MarketplacePage() {
return (
<div
key={agent.agent_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"
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"
>
<div className={`h-50 flex items-center justify-center relative`} style={{ backgroundColor: color }}>
<div className="text-4xl">
@ -207,7 +221,7 @@ export default function MarketplacePage() {
</div>
</div>
<div className="p-4">
<div className="p-4 flex flex-col flex-1">
<div className="flex items-center gap-2 mb-1">
<h3 className="text-foreground font-medium text-lg line-clamp-1 flex-1">
{agent.name}
@ -249,7 +263,7 @@ export default function MarketplacePage() {
handleAddToLibrary(agent.agent_id, agent.name);
}}
disabled={addingAgentId === agent.agent_id}
className="w-full transition-opacity"
className="w-full transition-opacity mt-auto"
size="sm"
key={agent.agent_id}
>
@ -271,6 +285,15 @@ export default function MarketplacePage() {
})}
</div>
)}
{pagination && pagination.pages > 1 && (
<Pagination
currentPage={pagination.page}
totalPages={pagination.pages}
onPageChange={setPage}
isLoading={isLoading}
/>
)}
</div>
</div>
);

View File

@ -29,18 +29,24 @@ export function AgentSelector({
className,
variant = 'default'
}: AgentSelectorProps) {
const { data: agents = [], isLoading, refetch: loadAgents } = useAgents();
const { data: agentsResponse, isLoading, refetch: loadAgents } = useAgents({
limit: 100,
sort_by: 'name',
sort_order: 'asc'
});
const router = useRouter();
const [isOpen, setIsOpen] = useState(false);
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const agents = agentsResponse?.agents || [];
const defaultAgent = agents.find(agent => agent.is_default);
const currentAgent = selectedAgentId
? agents.find(agent => agent.agent_id === selectedAgentId)
: null;
// Display name logic: show selected agent, default agent, or "Suna" as fallback
const displayName = currentAgent?.name || defaultAgent?.name || 'Suna';
const agentAvatar = currentAgent?.avatar;
const isUsingSuna = !currentAgent && !defaultAgent;
const handleAgentSelect = (agentId: string | undefined) => {
@ -96,6 +102,9 @@ export function AgentSelector({
>
<span className="underline decoration-dashed underline-offset-6 decoration-muted-foreground/50 tracking-tight text-4xl font-semibold leading-tight text-primary">
{displayName}
<span className="text-muted-foreground ml-2">
{agentAvatar && agentAvatar}
</span>
</span>
<div className="flex items-center opacity-60 group-hover:opacity-100 transition-opacity">
<ChevronDown className="h-5 w-5 text-muted-foreground" />

View File

@ -2,17 +2,19 @@ import { createMutationHook, createQueryHook } from '@/hooks/use-query';
import { useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { agentKeys } from './keys';
import { Agent, AgentUpdateRequest, createAgent, deleteAgent, getAgent, getAgents, getThreadAgent, updateAgent, AgentBuilderChatRequest, AgentBuilderStreamData, startAgentBuilderChat, getAgentBuilderChatHistory } from './utils';
import { Agent, AgentUpdateRequest, AgentsParams, createAgent, deleteAgent, getAgent, getAgents, getThreadAgent, updateAgent, AgentBuilderChatRequest, AgentBuilderStreamData, startAgentBuilderChat, getAgentBuilderChatHistory } from './utils';
import { useRef, useCallback } from 'react';
export const useAgents = createQueryHook(
agentKeys.list(),
getAgents,
{
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
}
);
export const useAgents = (params: AgentsParams = {}) => {
return createQueryHook(
agentKeys.list(params),
() => getAgents(params),
{
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
}
)();
};
export const useAgent = (agentId: string) => {
return createQueryHook(

View File

@ -24,6 +24,30 @@ export type Agent = {
avatar_color?: string;
};
export type PaginationInfo = {
page: number;
limit: number;
total: number;
pages: number;
};
export type AgentsResponse = {
agents: Agent[];
pagination: PaginationInfo;
};
export type AgentsParams = {
page?: number;
limit?: number;
search?: string;
sort_by?: string;
sort_order?: string;
has_default?: boolean;
has_mcp_tools?: boolean;
has_agentpress_tools?: boolean;
tools?: string;
};
export type ThreadAgentResponse = {
agent: Agent | null;
source: 'thread' | 'default' | 'none' | 'missing';
@ -54,7 +78,7 @@ export type AgentUpdateRequest = {
is_default?: boolean;
};
export const getAgents = async (): Promise<Agent[]> => {
export const getAgents = async (params: AgentsParams = {}): Promise<AgentsResponse> => {
try {
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
@ -63,7 +87,20 @@ export const getAgents = async (): Promise<Agent[]> => {
throw new Error('You must be logged in to get agents');
}
const response = await fetch(`${API_URL}/agents`, {
const queryParams = new URLSearchParams();
if (params.page) queryParams.append('page', params.page.toString());
if (params.limit) queryParams.append('limit', params.limit.toString());
if (params.search) queryParams.append('search', params.search);
if (params.sort_by) queryParams.append('sort_by', params.sort_by);
if (params.sort_order) queryParams.append('sort_order', params.sort_order);
if (params.has_default !== undefined) queryParams.append('has_default', params.has_default.toString());
if (params.has_mcp_tools !== undefined) queryParams.append('has_mcp_tools', params.has_mcp_tools.toString());
if (params.has_agentpress_tools !== undefined) queryParams.append('has_agentpress_tools', params.has_agentpress_tools.toString());
if (params.tools) queryParams.append('tools', params.tools);
const url = `${API_URL}/agents${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
@ -76,9 +113,9 @@ export const getAgents = async (): Promise<Agent[]> => {
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
}
const agents = await response.json();
console.log('[API] Fetched agents:', agents.length);
return agents;
const result = await response.json();
console.log('[API] Fetched agents:', result.agents?.length || 0, 'total:', result.pagination?.total || 0);
return result;
} catch (err) {
console.error('Error fetching agents:', err);
throw err;

View File

@ -19,29 +19,44 @@ export interface MarketplaceAgent {
avatar_color?: string;
}
export interface PaginationInfo {
page: number;
limit: number;
total: number;
pages: number;
}
export interface MarketplaceAgentsResponse {
agents: MarketplaceAgent[];
pagination: PaginationInfo;
}
interface MarketplaceAgentsParams {
page?: number;
limit?: number;
search?: string;
tags?: string[];
sort?: 'newest' | 'popular' | 'most_downloaded';
limit?: number;
offset?: number;
sort_by?: 'newest' | 'popular' | 'most_downloaded' | 'name';
creator?: string;
}
export function useMarketplaceAgents(params: MarketplaceAgentsParams = {}) {
return useQuery({
queryKey: ['marketplace-agents', params],
queryFn: async (): Promise<MarketplaceAgent[]> => {
queryFn: async (): Promise<MarketplaceAgentsResponse> => {
try {
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
const queryParams = new URLSearchParams();
if (params.page) queryParams.append('page', params.page.toString());
if (params.limit) queryParams.append('limit', params.limit.toString());
if (params.search) queryParams.append('search', params.search);
if (params.tags && params.tags.length > 0) {
queryParams.append('tags', params.tags.join(','));
}
if (params.limit) queryParams.append('limit', params.limit.toString());
if (params.offset) queryParams.append('offset', params.offset.toString());
if (params.sort_by) queryParams.append('sort_by', params.sort_by);
if (params.creator) queryParams.append('creator', params.creator);
const url = `${API_URL}/marketplace/agents${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
@ -64,25 +79,8 @@ export function useMarketplaceAgents(params: MarketplaceAgentsParams = {}) {
}
const data = await response.json();
let agents = data.agents || [];
// Sort the results based on the sort parameter
switch (params.sort) {
case 'most_downloaded':
agents = agents.sort((a, b) => b.download_count - a.download_count);
break;
case 'popular':
agents = agents.sort((a, b) => b.download_count - a.download_count);
break;
case 'newest':
default:
agents = agents.sort((a, b) =>
new Date(b.marketplace_published_at).getTime() - new Date(a.marketplace_published_at).getTime()
);
break;
}
return agents;
console.log('[API] Fetched marketplace agents:', data.agents?.length || 0, 'total:', data.pagination?.total || 0);
return data;
} catch (err) {
console.error('Error fetching marketplace agents:', err);
throw err;