From 7ce20533a6d48c344a4a3b0cc5fd4b093de06364 Mon Sep 17 00:00:00 2001 From: Soumyadas15 Date: Sun, 1 Jun 2025 02:51:13 +0530 Subject: [PATCH] feat: agentbuilder v1 --- PAGINATION_IMPLEMENTATION.md | 130 +++++++++++ backend/agent/api.py | 219 ++++++++++++++++-- .../agents/_components/pagination.tsx | 91 ++++++++ .../agents/_components/results-info.tsx | 15 +- .../(dashboard)/agents/new/[agentId]/page.tsx | 11 + frontend/src/app/(dashboard)/agents/page.tsx | 136 +++++++++-- .../src/app/(dashboard)/marketplace/page.tsx | 45 +++- .../components/dashboard/agent-selector.tsx | 13 +- .../hooks/react-query/agents/use-agents.ts | 20 +- .../src/hooks/react-query/agents/utils.ts | 47 +++- .../marketplace/use-marketplace.ts | 48 ++-- 11 files changed, 673 insertions(+), 102 deletions(-) create mode 100644 PAGINATION_IMPLEMENTATION.md create mode 100644 frontend/src/app/(dashboard)/agents/_components/pagination.tsx diff --git a/PAGINATION_IMPLEMENTATION.md b/PAGINATION_IMPLEMENTATION.md new file mode 100644 index 00000000..9b9bb432 --- /dev/null +++ b/PAGINATION_IMPLEMENTATION.md @@ -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 \ No newline at end of file diff --git a/backend/agent/api.py b/backend/agent/api.py index 800e808f..379875f0 100644 --- a/backend/agent/api.py +++ b/backend/agent/api.py @@ -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)}") diff --git a/frontend/src/app/(dashboard)/agents/_components/pagination.tsx b/frontend/src/app/(dashboard)/agents/_components/pagination.tsx new file mode 100644 index 00000000..04f0a9db --- /dev/null +++ b/frontend/src/app/(dashboard)/agents/_components/pagination.tsx @@ -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 = ({ + 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 ( +
+ + + {visiblePages.map((page, index) => ( + + {page === '...' ? ( +
+ +
+ ) : ( + + )} +
+ ))} + + +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/app/(dashboard)/agents/_components/results-info.tsx b/frontend/src/app/(dashboard)/agents/_components/results-info.tsx index 217535d7..6fbdabb0 100644 --- a/frontend/src/app/(dashboard)/agents/_components/results-info.tsx +++ b/frontend/src/app/(dashboard)/agents/_components/results-info.tsx @@ -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 (
- Showing {filteredCount} of {totalAgents} agents + {showingText()} {searchQuery && ` for "${searchQuery}"`} {activeFiltersCount > 0 && ( diff --git a/frontend/src/app/(dashboard)/agents/new/[agentId]/page.tsx b/frontend/src/app/(dashboard)/agents/new/[agentId]/page.tsx index 6c875f9a..0d766e6a 100644 --- a/frontend/src/app/(dashboard)/agents/new/[agentId]/page.tsx +++ b/frontend/src/app/(dashboard)/agents/new/[agentId]/page.tsx @@ -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(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; diff --git a/frontend/src/app/(dashboard)/agents/page.tsx b/frontend/src/app/(dashboard)/agents/page.tsx index 671826c4..db49b818 100644 --- a/frontend/src/app/(dashboard)/agents/page.tsx +++ b/frontend/src/app/(dashboard)/agents/page.tsx @@ -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(null); const [viewMode, setViewMode] = useState('grid'); + + // Server-side parameters + const [page, setPage] = useState(1); + const [searchQuery, setSearchQuery] = useState(''); + const [sortBy, setSortBy] = useState('created_at'); + const [sortOrder, setSortOrder] = useState('desc'); + const [filters, setFilters] = useState({ + 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(); + 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 ? ( <> -
+ Creating... ) : ( @@ -163,24 +238,26 @@ export default function AgentsPage() { {isLoading ? ( - ) : filteredAndSortedAgents.length === 0 ? ( + ) : agents.length === 0 ? ( 0} + hasAgents={(pagination?.total || 0) > 0} onCreateAgent={handleCreateNewAgent} onClearFilters={clearFilters} /> ) : ( )} + {pagination && pagination.pages > 1 && ( + + )} + ([]); const [sortBy, setSortBy] = useState('newest'); const [addingAgentId, setAddingAgentId] = useState(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() {
{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 ? (
- {Array.from({ length: 4 }).map((_, i) => ( + {Array.from({ length: 8 }).map((_, i) => (
@@ -193,7 +207,7 @@ export default function MarketplacePage() { return (
@@ -207,7 +221,7 @@ export default function MarketplacePage() {
-
+

{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() { })}

)} + + {pagination && pagination.pages > 1 && ( + + )}
); diff --git a/frontend/src/components/dashboard/agent-selector.tsx b/frontend/src/components/dashboard/agent-selector.tsx index 30438701..6bf58337 100644 --- a/frontend/src/components/dashboard/agent-selector.tsx +++ b/frontend/src/components/dashboard/agent-selector.tsx @@ -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({ > {displayName} + + {agentAvatar && agentAvatar} +
diff --git a/frontend/src/hooks/react-query/agents/use-agents.ts b/frontend/src/hooks/react-query/agents/use-agents.ts index 93f98aba..10ca51b6 100644 --- a/frontend/src/hooks/react-query/agents/use-agents.ts +++ b/frontend/src/hooks/react-query/agents/use-agents.ts @@ -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( diff --git a/frontend/src/hooks/react-query/agents/utils.ts b/frontend/src/hooks/react-query/agents/utils.ts index e0253084..893166fe 100644 --- a/frontend/src/hooks/react-query/agents/utils.ts +++ b/frontend/src/hooks/react-query/agents/utils.ts @@ -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 => { +export const getAgents = async (params: AgentsParams = {}): Promise => { try { const supabase = createClient(); const { data: { session } } = await supabase.auth.getSession(); @@ -63,7 +87,20 @@ export const getAgents = async (): Promise => { 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 => { 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; diff --git a/frontend/src/hooks/react-query/marketplace/use-marketplace.ts b/frontend/src/hooks/react-query/marketplace/use-marketplace.ts index dccc6340..d8912dd6 100644 --- a/frontend/src/hooks/react-query/marketplace/use-marketplace.ts +++ b/frontend/src/hooks/react-query/marketplace/use-marketplace.ts @@ -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 => { + queryFn: async (): Promise => { 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;