feat(frontend, backend): Publish Agent Dialog Agent List Pagination (#10023)

We want scrolling for agent dialog list

- Based on #9833

### Changes 🏗️
- adds backend support for paginating this content
- adds frontend support for scrolling pagination
<!-- Concisely describe all of the changes made in this pull request:
-->

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  <!-- Put your test plan here: -->
  - [x] test UI for this

---------

Co-authored-by: Venkat Sai Kedari Nath Gandham <154089422+Kedarinath1502@users.noreply.github.com>
Co-authored-by: Claude <claude@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Nicholas Tindle
2025-07-28 17:39:46 -05:00
committed by GitHub
parent 7ea4077dc6
commit f7c1906364
8 changed files with 265 additions and 74 deletions

View File

@@ -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")

View File

@@ -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<getV2GetMyAgentsResponse> => {
return customMutator<getV2GetMyAgentsResponse>(getGetV2GetMyAgentsUrl(), {
...options,
method: "GET",
});
return customMutator<getV2GetMyAgentsResponse>(
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<ReturnType<typeof getV2GetMyAgents>>,
TError = unknown,
>(options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getV2GetMyAgents>>, TError, TData>
>;
request?: SecondParameter<typeof customMutator>;
}) => {
TError = HTTPValidationError,
>(
params?: GetV2GetMyAgentsParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof getV2GetMyAgents>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customMutator>;
},
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetV2GetMyAgentsQueryKey();
const queryKey =
queryOptions?.queryKey ?? getGetV2GetMyAgentsQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getV2GetMyAgents>>
> = ({ signal }) => getV2GetMyAgents({ signal, ...requestOptions });
> = ({ signal }) => getV2GetMyAgents(params, { signal, ...requestOptions });
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getV2GetMyAgents>>,
@@ -2000,12 +2035,13 @@ export const getGetV2GetMyAgentsQueryOptions = <
export type GetV2GetMyAgentsQueryResult = NonNullable<
Awaited<ReturnType<typeof getV2GetMyAgents>>
>;
export type GetV2GetMyAgentsQueryError = unknown;
export type GetV2GetMyAgentsQueryError = HTTPValidationError;
export function useGetV2GetMyAgents<
TData = Awaited<ReturnType<typeof getV2GetMyAgents>>,
TError = unknown,
TError = HTTPValidationError,
>(
params: undefined | GetV2GetMyAgentsParams,
options: {
query: Partial<
UseQueryOptions<
@@ -2030,8 +2066,9 @@ export function useGetV2GetMyAgents<
};
export function useGetV2GetMyAgents<
TData = Awaited<ReturnType<typeof getV2GetMyAgents>>,
TError = unknown,
TError = HTTPValidationError,
>(
params?: GetV2GetMyAgentsParams,
options?: {
query?: Partial<
UseQueryOptions<
@@ -2056,8 +2093,9 @@ export function useGetV2GetMyAgents<
};
export function useGetV2GetMyAgents<
TData = Awaited<ReturnType<typeof getV2GetMyAgents>>,
TError = unknown,
TError = HTTPValidationError,
>(
params?: GetV2GetMyAgentsParams,
options?: {
query?: Partial<
UseQueryOptions<
@@ -2078,8 +2116,9 @@ export function useGetV2GetMyAgents<
export function useGetV2GetMyAgents<
TData = Awaited<ReturnType<typeof getV2GetMyAgents>>,
TError = unknown,
TError = HTTPValidationError,
>(
params?: GetV2GetMyAgentsParams,
options?: {
query?: Partial<
UseQueryOptions<
@@ -2094,7 +2133,7 @@ export function useGetV2GetMyAgents<
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
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<ReturnType<typeof getV2GetMyAgents>>,
TError = unknown,
TError = HTTPValidationError,
>(
queryClient: QueryClient,
params?: GetV2GetMyAgentsParams,
options?: {
query?: Partial<
UseQueryOptions<
@@ -2125,7 +2165,7 @@ export const prefetchGetV2GetMyAgentsQuery = async <
request?: SecondParameter<typeof customMutator>;
},
): Promise<QueryClient> => {
const queryOptions = getGetV2GetMyAgentsQueryOptions(options);
const queryOptions = getGetV2GetMyAgentsQueryOptions(params, options);
await queryClient.prefetchQuery(queryOptions);

View File

@@ -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;
};

View File

@@ -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" }
}
}
}
}
}

View File

@@ -20,6 +20,7 @@ interface PublishAgentSelectProps {
onNext: (agentId: string, agentVersion: number) => void;
onClose: () => void;
onOpenBuilder: () => void;
onListScroll?: (e: React.UIEvent<HTMLDivElement>) => void;
}
export const PublishAgentSelect: React.FC<PublishAgentSelectProps> = ({
@@ -29,6 +30,7 @@ export const PublishAgentSelect: React.FC<PublishAgentSelectProps> = ({
onNext,
onClose,
onOpenBuilder,
onListScroll,
}) => {
const [selectedAgentId, setSelectedAgentId] = React.useState<string | null>(
null,
@@ -93,9 +95,11 @@ export const PublishAgentSelect: React.FC<PublishAgentSelectProps> = ({
<div className="flex-grow overflow-hidden p-4 sm:p-6">
<h3 className="sr-only">List of agents</h3>
<div
className="h-[300px] overflow-y-auto pr-2 sm:h-[400px] md:h-[500px]"
className="h-[300px] overflow-y-auto overscroll-contain pr-2 sm:h-[400px] md:h-[500px]"
role="region"
aria-labelledby="agentListHeading"
onScroll={onListScroll}
onWheel={(e) => e.stopPropagation()}
>
<div id="agentListHeading" className="sr-only">
Scrollable list of agents

View File

@@ -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<PublishAgentPopoutProps> = ({
const [step, setStep] = React.useState<"select" | "info" | "review">(
inputStep,
);
const [myAgents, setMyAgents] = React.useState<MyAgentsResponse | null>(null);
const [allAgents, setAllAgents] = React.useState<MyAgent[]>([]);
const [_, setSelectedAgent] = React.useState<string | null>(null);
const [initialData, setInitialData] =
React.useState<PublishAgentInfoInitialData>({
@@ -66,10 +68,54 @@ export const PublishAgentPopout: React.FC<PublishAgentPopoutProps> = ({
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<PublishAgentPopoutProps> = ({
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<PublishAgentPopoutProps> = ({
};
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<PublishAgentPopoutProps> = ({
}
};
const handleScroll = React.useCallback(
(e: React.UIEvent<HTMLDivElement>) => {
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 (
<div className="flex min-h-screen items-center justify-center">
<div className="mx-auto flex w-full max-w-[900px] flex-col rounded-3xl bg-white shadow-lg dark:bg-gray-800">
<div className="h-full overflow-y-auto">
<PublishAgentSelect
agents={
myAgents?.agents
.map((agent) => ({
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")}
/>
<div className="h-full overflow-y-hidden">
{isLoading && currentPage === 1 ? (
<LoadingBox className="p-8" />
) : error ? (
<div className="flex flex-col items-center justify-center gap-4 p-8">
<p className="text-red-600">
Failed to load agents. Please try again.
</p>
<Button
onClick={() => {
refetch();
}}
variant="outline"
>
Try Again
</Button>
</div>
) : (
<>
<PublishAgentSelect
agents={
allAgents
.map((agent) => ({
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 && (
<div className="flex items-center justify-center p-4">
<LoadingSpinner className="size-6" />
</div>
)}
</>
)}
</div>
</div>
</div>

View File

@@ -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)