diff --git a/autogpt_platform/backend/backend/server/v2/store/routes.py b/autogpt_platform/backend/backend/server/v2/store/routes.py index 6cc2721968..152df1a4b7 100644 --- a/autogpt_platform/backend/backend/server/v2/store/routes.py +++ b/autogpt_platform/backend/backend/server/v2/store/routes.py @@ -409,9 +409,13 @@ async def get_my_agents( user_id: typing.Annotated[ str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id) ], + page: typing.Annotated[int, fastapi.Query(ge=1)] = 1, + page_size: typing.Annotated[int, fastapi.Query(ge=1)] = 20, ): try: - agents = await backend.server.v2.store.db.get_my_agents(user_id) + agents = await backend.server.v2.store.db.get_my_agents( + user_id, page=page, page_size=page_size + ) return agents except Exception: logger.exception("Exception occurred whilst getting my agents") diff --git a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/store/store.ts b/autogpt_platform/frontend/src/app/api/__generated__/endpoints/store/store.ts index a6b379665f..c6e39a3808 100644 --- a/autogpt_platform/frontend/src/app/api/__generated__/endpoints/store/store.ts +++ b/autogpt_platform/frontend/src/app/api/__generated__/endpoints/store/store.ts @@ -27,6 +27,8 @@ import type { CreatorDetails } from "../../models/creatorDetails"; import type { CreatorsResponse } from "../../models/creatorsResponse"; +import type { GetV2GetMyAgentsParams } from "../../models/getV2GetMyAgentsParams"; + import type { GetV2ListMySubmissionsParams } from "../../models/getV2ListMySubmissionsParams"; import type { GetV2ListStoreAgentsParams } from "../../models/getV2ListStoreAgentsParams"; @@ -1950,45 +1952,78 @@ export type getV2GetMyAgentsResponse200 = { status: 200; }; -export type getV2GetMyAgentsResponseComposite = getV2GetMyAgentsResponse200; +export type getV2GetMyAgentsResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type getV2GetMyAgentsResponseComposite = + | getV2GetMyAgentsResponse200 + | getV2GetMyAgentsResponse422; export type getV2GetMyAgentsResponse = getV2GetMyAgentsResponseComposite & { headers: Headers; }; -export const getGetV2GetMyAgentsUrl = () => { - return `/api/store/myagents`; +export const getGetV2GetMyAgentsUrl = (params?: GetV2GetMyAgentsParams) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/store/myagents?${stringifiedParams}` + : `/api/store/myagents`; }; export const getV2GetMyAgents = async ( + params?: GetV2GetMyAgentsParams, options?: RequestInit, ): Promise => { - return customMutator(getGetV2GetMyAgentsUrl(), { - ...options, - method: "GET", - }); + return customMutator( + getGetV2GetMyAgentsUrl(params), + { + ...options, + method: "GET", + }, + ); }; -export const getGetV2GetMyAgentsQueryKey = () => { - return [`/api/store/myagents`] as const; +export const getGetV2GetMyAgentsQueryKey = ( + params?: GetV2GetMyAgentsParams, +) => { + return [`/api/store/myagents`, ...(params ? [params] : [])] as const; }; export const getGetV2GetMyAgentsQueryOptions = < TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions>, TError, TData> - >; - request?: SecondParameter; -}) => { + TError = HTTPValidationError, +>( + params?: GetV2GetMyAgentsParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, +) => { const { query: queryOptions, request: requestOptions } = options ?? {}; - const queryKey = queryOptions?.queryKey ?? getGetV2GetMyAgentsQueryKey(); + const queryKey = + queryOptions?.queryKey ?? getGetV2GetMyAgentsQueryKey(params); const queryFn: QueryFunction< Awaited> - > = ({ signal }) => getV2GetMyAgents({ signal, ...requestOptions }); + > = ({ signal }) => getV2GetMyAgents(params, { signal, ...requestOptions }); return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< Awaited>, @@ -2000,12 +2035,13 @@ export const getGetV2GetMyAgentsQueryOptions = < export type GetV2GetMyAgentsQueryResult = NonNullable< Awaited> >; -export type GetV2GetMyAgentsQueryError = unknown; +export type GetV2GetMyAgentsQueryError = HTTPValidationError; export function useGetV2GetMyAgents< TData = Awaited>, - TError = unknown, + TError = HTTPValidationError, >( + params: undefined | GetV2GetMyAgentsParams, options: { query: Partial< UseQueryOptions< @@ -2030,8 +2066,9 @@ export function useGetV2GetMyAgents< }; export function useGetV2GetMyAgents< TData = Awaited>, - TError = unknown, + TError = HTTPValidationError, >( + params?: GetV2GetMyAgentsParams, options?: { query?: Partial< UseQueryOptions< @@ -2056,8 +2093,9 @@ export function useGetV2GetMyAgents< }; export function useGetV2GetMyAgents< TData = Awaited>, - TError = unknown, + TError = HTTPValidationError, >( + params?: GetV2GetMyAgentsParams, options?: { query?: Partial< UseQueryOptions< @@ -2078,8 +2116,9 @@ export function useGetV2GetMyAgents< export function useGetV2GetMyAgents< TData = Awaited>, - TError = unknown, + TError = HTTPValidationError, >( + params?: GetV2GetMyAgentsParams, options?: { query?: Partial< UseQueryOptions< @@ -2094,7 +2133,7 @@ export function useGetV2GetMyAgents< ): UseQueryResult & { queryKey: DataTag; } { - const queryOptions = getGetV2GetMyAgentsQueryOptions(options); + const queryOptions = getGetV2GetMyAgentsQueryOptions(params, options); const query = useQuery(queryOptions, queryClient) as UseQueryResult< TData, @@ -2111,9 +2150,10 @@ export function useGetV2GetMyAgents< */ export const prefetchGetV2GetMyAgentsQuery = async < TData = Awaited>, - TError = unknown, + TError = HTTPValidationError, >( queryClient: QueryClient, + params?: GetV2GetMyAgentsParams, options?: { query?: Partial< UseQueryOptions< @@ -2125,7 +2165,7 @@ export const prefetchGetV2GetMyAgentsQuery = async < request?: SecondParameter; }, ): Promise => { - const queryOptions = getGetV2GetMyAgentsQueryOptions(options); + const queryOptions = getGetV2GetMyAgentsQueryOptions(params, options); await queryClient.prefetchQuery(queryOptions); diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/getV2GetMyAgentsParams.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/getV2GetMyAgentsParams.ts new file mode 100644 index 0000000000..2604c985e6 --- /dev/null +++ b/autogpt_platform/frontend/src/app/api/__generated__/models/getV2GetMyAgentsParams.ts @@ -0,0 +1,18 @@ +/** + * Generated by orval v7.10.0 🍺 + * Do not edit manually. + * AutoGPT Agent Server + * This server is used to execute agents that are created by the AutoGPT system. + * OpenAPI spec version: 0.1 + */ + +export type GetV2GetMyAgentsParams = { + /** + * @minimum 1 + */ + page?: number; + /** + * @minimum 1 + */ + page_size?: number; +}; diff --git a/autogpt_platform/frontend/src/app/api/openapi.json b/autogpt_platform/frontend/src/app/api/openapi.json index a03f764e0d..15bf9c4d41 100644 --- a/autogpt_platform/frontend/src/app/api/openapi.json +++ b/autogpt_platform/frontend/src/app/api/openapi.json @@ -2467,6 +2467,30 @@ "tags": ["v2", "store", "private"], "summary": "Get my agents", "operationId": "getV2Get my agents", + "parameters": [ + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "default": 1, + "title": "Page" + } + }, + { + "name": "page_size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "default": 20, + "title": "Page Size" + } + } + ], "responses": { "200": { "description": "Successful Response", @@ -2475,6 +2499,14 @@ "schema": { "$ref": "#/components/schemas/MyAgentsResponse" } } } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/HTTPValidationError" } + } + } } } } diff --git a/autogpt_platform/frontend/src/components/agptui/PublishAgentSelect.tsx b/autogpt_platform/frontend/src/components/agptui/PublishAgentSelect.tsx index f89d6a856e..f999ceb15b 100644 --- a/autogpt_platform/frontend/src/components/agptui/PublishAgentSelect.tsx +++ b/autogpt_platform/frontend/src/components/agptui/PublishAgentSelect.tsx @@ -20,6 +20,7 @@ interface PublishAgentSelectProps { onNext: (agentId: string, agentVersion: number) => void; onClose: () => void; onOpenBuilder: () => void; + onListScroll?: (e: React.UIEvent) => void; } export const PublishAgentSelect: React.FC = ({ @@ -29,6 +30,7 @@ export const PublishAgentSelect: React.FC = ({ onNext, onClose, onOpenBuilder, + onListScroll, }) => { const [selectedAgentId, setSelectedAgentId] = React.useState( null, @@ -93,9 +95,11 @@ export const PublishAgentSelect: React.FC = ({

List of agents

e.stopPropagation()} >
Scrollable list of agents diff --git a/autogpt_platform/frontend/src/components/agptui/composite/PublishAgentPopout.tsx b/autogpt_platform/frontend/src/components/agptui/composite/PublishAgentPopout.tsx index 3bb6bf53a7..55163cc485 100644 --- a/autogpt_platform/frontend/src/components/agptui/composite/PublishAgentPopout.tsx +++ b/autogpt_platform/frontend/src/components/agptui/composite/PublishAgentPopout.tsx @@ -14,11 +14,13 @@ import { } from "../PublishAgentSelectInfo"; import { PublishAgentAwaitingReview } from "../PublishAgentAwaitingReview"; import { Button } from "../Button"; -import { MyAgentsResponse } from "@/lib/autogpt-server-api"; +import { MyAgent } from "@/app/api/__generated__/models/myAgent"; import { useRouter } from "next/navigation"; import { useBackendAPI } from "@/lib/autogpt-server-api/context"; import { useToast } from "@/components/molecules/Toast/use-toast"; +import LoadingBox, { LoadingSpinner } from "@/components/ui/loading"; import { StoreSubmissionRequest } from "@/app/api/__generated__/models/storeSubmissionRequest"; +import { useGetV2GetMyAgents } from "@/app/api/__generated__/endpoints/store/store"; interface PublishAgentPopoutProps { trigger?: React.ReactNode; openPopout?: boolean; @@ -44,7 +46,7 @@ export const PublishAgentPopout: React.FC = ({ const [step, setStep] = React.useState<"select" | "info" | "review">( inputStep, ); - const [myAgents, setMyAgents] = React.useState(null); + const [allAgents, setAllAgents] = React.useState([]); const [_, setSelectedAgent] = React.useState(null); const [initialData, setInitialData] = React.useState({ @@ -66,10 +68,54 @@ export const PublishAgentPopout: React.FC = ({ number | null >(null); const [open, setOpen] = React.useState(false); + const [currentPage, setCurrentPage] = React.useState(1); + const [loadingMore, setLoadingMore] = React.useState(false); + const [hasMore, setHasMore] = React.useState(true); + + const api = useBackendAPI(); + + // Use the auto-generated API hook + const { data, error, isLoading, refetch } = useGetV2GetMyAgents( + { + page: currentPage, + page_size: 20, + }, + { + query: { + enabled: open, // Only fetch when the popout is open + }, + }, + ); + + // Update allAgents when new data arrives + React.useEffect(() => { + if (data?.status === 200 && data.data) { + if (currentPage === 1) { + setAllAgents(data.data.agents); + } else { + setAllAgents((prev) => [...prev, ...data.data.agents]); + } + setHasMore( + data.data.pagination.current_page < data.data.pagination.total_pages, + ); + } + }, [data, currentPage]); + + const fetchMyAgents = React.useCallback( + async (page: number, append = false) => { + if (append) { + setLoadingMore(true); + setCurrentPage(page); + } else { + setCurrentPage(page); + setAllAgents([]); + } + }, + [], + ); const popupId = React.useId(); const router = useRouter(); - const api = useBackendAPI(); const { toast } = useToast(); @@ -81,18 +127,18 @@ export const PublishAgentPopout: React.FC = ({ React.useEffect(() => { if (open) { - const loadMyAgents = async () => { - try { - const response = await api.getMyAgents(); - setMyAgents(response); - } catch (error) { - console.error("Failed to load my agents:", error); - } - }; - - loadMyAgents(); + setCurrentPage(1); + setHasMore(true); + setAllAgents([]); } - }, [open, api]); + }, [open]); + + // Handle loading state for pagination + React.useEffect(() => { + if (currentPage > 1 && !isLoading) { + setLoadingMore(false); + } + }, [currentPage, isLoading]); const handleClose = () => { setStep("select"); @@ -115,9 +161,9 @@ export const PublishAgentPopout: React.FC = ({ }; const handleNextFromSelect = (agentId: string, agentVersion: number) => { - const selectedAgentData = myAgents?.agents.find( + const selectedAgentData = allAgents.find( (agent) => agent.agent_id === agentId, - ); + ) as any; const name = selectedAgentData?.agent_name || ""; const description = selectedAgentData?.description || ""; @@ -204,36 +250,77 @@ export const PublishAgentPopout: React.FC = ({ } }; + const handleScroll = React.useCallback( + (e: React.UIEvent) => { + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; + if ( + hasMore && + !loadingMore && + scrollTop + clientHeight >= scrollHeight - 50 + ) { + fetchMyAgents(currentPage + 1, true); + } + }, + [hasMore, loadingMore, currentPage], + ); + const renderContent = () => { switch (step) { case "select": return (
-
- ({ - name: agent.agent_name, - id: agent.agent_id, - version: agent.agent_version, - lastEdited: agent.last_edited, - imageSrc: - agent.agent_image || "https://picsum.photos/300/200", - })) - .sort( - (a, b) => - new Date(b.lastEdited).getTime() - - new Date(a.lastEdited).getTime(), - ) || [] - } - onSelect={handleAgentSelect} - onCancel={handleClose} - onNext={handleNextFromSelect} - onClose={handleClose} - onOpenBuilder={() => router.push("/build")} - /> +
+ {isLoading && currentPage === 1 ? ( + + ) : error ? ( +
+

+ Failed to load agents. Please try again. +

+ +
+ ) : ( + <> + ({ + name: agent.agent_name, + id: agent.agent_id, + version: agent.agent_version, + lastEdited: agent.last_edited, + imageSrc: + agent.agent_image || + "https://picsum.photos/300/200", + })) + .sort( + (a, b) => + new Date(b.lastEdited).getTime() - + new Date(a.lastEdited).getTime(), + ) || [] + } + onSelect={handleAgentSelect} + onCancel={handleClose} + onNext={handleNextFromSelect} + onClose={handleClose} + onOpenBuilder={() => router.push("/build")} + onListScroll={handleScroll} + /> + {loadingMore && ( +
+ +
+ )} + + )}
diff --git a/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/useAgentActivityDropdown.ts b/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/useAgentActivityDropdown.ts index 025bc7b94a..2f075b4990 100644 --- a/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/useAgentActivityDropdown.ts +++ b/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/useAgentActivityDropdown.ts @@ -33,11 +33,15 @@ export function useAgentActivityDropdown() { data: myAgentsResponse, isLoading: isAgentsLoading, error: agentsError, - } = useGetV2GetMyAgents({ - query: { - enabled: true, + } = useGetV2GetMyAgents( + {}, + { + // Enable query by default + query: { + enabled: true, + }, }, - }); + ); // Get library agents data to map graph_id to library_agent_id const { @@ -67,6 +71,8 @@ export function useAgentActivityDropdown() { // Update agent info map when both agent data sources change useEffect(() => { if ( + myAgentsResponse?.data && + "agents" in myAgentsResponse.data && myAgentsResponse?.data?.agents && libraryAgentsResponse?.data && "agents" in libraryAgentsResponse.data @@ -122,7 +128,7 @@ export function useAgentActivityDropdown() { setIsConnected(true); // Subscribe to graph executions for all user agents - if (myAgentsResponse?.data?.agents) { + if (myAgentsResponse?.data && "agents" in myAgentsResponse.data) { myAgentsResponse.data.agents.forEach((agent) => { api .subscribeToGraphExecutions(agent.agent_id as any) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index adcbb213cd..1d7b17e811 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -57,7 +57,7 @@ nav: - Contribute: - Introduction: contribute/index.md - - Testing: autogpt_platform/backend/TESTING.md + - Testing: ../../autogpt_platform/backend/TESTING.md # - Challenges: # - Introduction: challenges/introduction.md