Merge 'dev' into 'cursor/update-login-and-signup-pages-931a'

This commit is contained in:
Lluis Agusti
2025-06-30 17:54:01 +04:00
21 changed files with 1313 additions and 694 deletions

View File

@@ -63,9 +63,147 @@ Every time a new Front-end dependency is added by you or others, you will need t
- `pnpm type-check` - Run TypeScript type checking
- `pnpm test` - Run Playwright tests
- `pnpm test-ui` - Run Playwright tests with UI
- `pnpm fetch:openapi` - Fetch OpenAPI spec from backend
- `pnpm generate:api-client` - Generate API client from OpenAPI spec
- `pnpm generate:api-all` - Fetch OpenAPI spec and generate API client
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## 🔄 Data Fetching Strategy
This project uses an auto-generated API client powered by [**Orval**](https://orval.dev/), which creates type-safe API clients from OpenAPI specifications.
### How It Works
1. **Backend Requirements**: Each API endpoint needs a summary and tag in the OpenAPI spec
2. **Operation ID Generation**: FastAPI generates operation IDs using the pattern `{method}{tag}{summary}`
3. **Spec Fetching**: The OpenAPI spec is fetched from `http://localhost:8006/openapi.json` and saved to the frontend
4. **Spec Transformation**: The OpenAPI spec is cleaned up using a custom transformer (see `autogpt_platform/frontend/src/app/api/transformers`)
5. **Client Generation**: Auto-generated client includes TypeScript types, API endpoints, and Zod schemas, organized by tags
### API Client Commands
```bash
# Fetch OpenAPI spec from backend and generate client
pnpm generate:api-all
# Only fetch the OpenAPI spec
pnpm fetch:openapi
# Only generate the client (after spec is fetched)
pnpm generate:api-client
```
### Using the Generated Client
The generated client provides React Query hooks for both queries and mutations:
#### Queries (GET requests)
```typescript
import { useGetV1GetNotificationPreferences } from "@/app/api/__generated__/endpoints/auth/auth";
const { data, isLoading, isError } = useGetV1GetNotificationPreferences({
query: {
select: (res) => res.data,
// Other React Query options
},
});
```
#### Mutations (POST, PUT, DELETE requests)
```typescript
import { useDeleteV2DeleteStoreSubmission } from "@/app/api/__generated__/endpoints/store/store";
import { getGetV2ListMySubmissionsQueryKey } from "@/app/api/__generated__/endpoints/store/store";
import { useQueryClient } from "@tanstack/react-query";
const queryClient = useQueryClient();
const { mutateAsync: deleteSubmission } = useDeleteV2DeleteStoreSubmission({
mutation: {
onSuccess: () => {
// Invalidate related queries to refresh data
queryClient.invalidateQueries({
queryKey: getGetV2ListMySubmissionsQueryKey(),
});
},
},
});
// Usage
await deleteSubmission({
submissionId: submission_id,
});
```
#### Server Actions
For server-side operations, you can also use the generated client functions directly:
```typescript
import { postV1UpdateNotificationPreferences } from "@/app/api/__generated__/endpoints/auth/auth";
// In a server action
const preferences = {
email: "user@example.com",
preferences: {
AGENT_RUN: true,
ZERO_BALANCE: false,
// ... other preferences
},
daily_limit: 0,
};
await postV1UpdateNotificationPreferences(preferences);
```
#### Server-Side Prefetching
For server-side components, you can prefetch data on the server and hydrate it in the client cache. This allows immediate access to cached data when queries are called:
```typescript
import { getQueryClient } from "@/lib/tanstack-query/getQueryClient";
import {
prefetchGetV2ListStoreAgentsQuery,
prefetchGetV2ListStoreCreatorsQuery
} from "@/app/api/__generated__/endpoints/store/store";
import { HydrationBoundary, dehydrate } from "@tanstack/react-query";
// In your server component
const queryClient = getQueryClient();
await Promise.all([
prefetchGetV2ListStoreAgentsQuery(queryClient, {
featured: true,
}),
prefetchGetV2ListStoreAgentsQuery(queryClient, {
sorted_by: "runs",
}),
prefetchGetV2ListStoreCreatorsQuery(queryClient, {
featured: true,
sorted_by: "num_agents",
}),
]);
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<MainMarkeplacePage />
</HydrationBoundary>
);
```
This pattern improves performance by serving pre-fetched data from the server while maintaining the benefits of client-side React Query features.
### Configuration
The Orval configuration is located in `autogpt_platform/frontend/orval.config.ts`. It generates two separate clients:
1. **autogpt_api_client**: React Query hooks for client-side data fetching
2. **autogpt_zod_schema**: Zod schemas for validation
For more details, see the [Orval documentation](https://orval.dev/) or check the configuration file.
## 🚚 Deploy
TODO

View File

@@ -31,6 +31,14 @@ export default defineConfig({
useMutation: true,
// Will add more as their use cases arise
},
operations: {
"getV2List library agents": {
query: {
useInfinite: true,
useInfiniteQueryParam: "page",
},
},
},
},
},
hooks: {

View File

@@ -1,6 +1,6 @@
// import LibraryNotificationDropdown from "./library-notification-dropdown";
import LibraryUploadAgentDialog from "./library-upload-agent-dialog";
import LibrarySearchBar from "./library-search-bar";
import LibraryUploadAgentDialog from "../LibraryUploadAgentDialog/LibraryUploadAgentDialog";
import LibrarySearchBar from "../LibrarySearchBar/LibrarySearchBar";
type LibraryActionHeaderProps = Record<string, never>;

View File

@@ -1,11 +1,14 @@
"use client";
import { useLibraryPageContext } from "@/app/(platform)/library/state-provider";
import LibrarySortMenu from "./library-sort-menu";
import LibrarySortMenu from "../LibrarySortMenu/LibrarySortMenu";
export default function LibraryActionSubHeader(): React.ReactNode {
const { agents } = useLibraryPageContext();
interface LibraryActionSubHeaderProps {
agentCount: number;
}
export default function LibraryActionSubHeader({
agentCount,
}: LibraryActionSubHeaderProps) {
return (
<div className="flex items-center justify-between pb-[10px]">
<div className="flex items-center gap-[10px] p-2">
@@ -13,7 +16,7 @@ export default function LibraryActionSubHeader(): React.ReactNode {
My agents
</span>
<span className="w-[70px] font-sans text-[14px] font-normal leading-6">
{agents.length} agents
{agentCount} agents
</span>
</div>
<LibrarySortMenu />

View File

@@ -1,7 +1,12 @@
import Link from "next/link";
import Image from "next/image";
import { LibraryAgent } from "@/lib/autogpt-server-api";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
interface LibraryAgentCardProps {
agent: LibraryAgent;
}
export default function LibraryAgentCard({
agent: {
@@ -13,9 +18,7 @@ export default function LibraryAgentCard({
creator_image_url,
image_url,
},
}: {
agent: LibraryAgent;
}): React.ReactNode {
}: LibraryAgentCardProps) {
return (
<div className="inline-flex w-full max-w-[434px] flex-col items-start justify-start gap-2.5 rounded-[26px] bg-white transition-all duration-300 hover:shadow-lg dark:bg-transparent dark:hover:shadow-gray-700">
<Link

View File

@@ -0,0 +1,44 @@
"use client";
import LibraryActionSubHeader from "../LibraryActionSubHeader/LibraryActionSubHeader";
import LibraryAgentCard from "../LibraryAgentCard/LibraryAgentCard";
import { useLibraryAgentList } from "./useLibraryAgentList";
export default function LibraryAgentList() {
const {
agentLoading,
allAgents: agents,
isFetchingNextPage,
isSearching,
} = useLibraryAgentList();
const LoadingSpinner = () => (
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-t-2 border-neutral-800" />
);
return (
<>
{/* TODO: We need a new endpoint on backend that returns total number of agents */}
<LibraryActionSubHeader agentCount={agents.length} />
<div className="px-2">
{agentLoading ? (
<div className="flex h-[200px] items-center justify-center">
<LoadingSpinner />
</div>
) : (
<>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{agents.map((agent) => (
<LibraryAgentCard key={agent.id} agent={agent} />
))}
</div>
{(isFetchingNextPage || isSearching) && (
<div className="flex items-center justify-center py-4 pt-8">
<LoadingSpinner />
</div>
)}
</>
)}
</div>
</>
);
}

View File

@@ -0,0 +1,66 @@
import { useGetV2ListLibraryAgentsInfinite } from "@/app/api/__generated__/endpoints/library/library";
import { LibraryAgentResponse } from "@/app/api/__generated__/models/libraryAgentResponse";
import { useScrollThreshold } from "@/hooks/useScrollThreshold";
import { useCallback } from "react";
import { useLibraryPageContext } from "../state-provider";
export const useLibraryAgentList = () => {
const { searchTerm, librarySort } = useLibraryPageContext();
const {
data: agents,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading: agentLoading,
isFetching,
} = useGetV2ListLibraryAgentsInfinite(
{
page: 1,
page_size: 8,
search_term: searchTerm || undefined,
sort_by: librarySort,
},
{
query: {
getNextPageParam: (lastPage) => {
const pagination = (lastPage.data as LibraryAgentResponse).pagination;
const isMore =
pagination.current_page * pagination.page_size <
pagination.total_items;
return isMore ? pagination.current_page + 1 : undefined;
},
},
},
);
const handleInfiniteScroll = useCallback(
(scrollY: number) => {
if (!hasNextPage || isFetchingNextPage) return;
const { scrollHeight, clientHeight } = document.documentElement;
const SCROLL_THRESHOLD = 20;
if (scrollY + clientHeight >= scrollHeight - SCROLL_THRESHOLD) {
fetchNextPage();
}
},
[hasNextPage, isFetchingNextPage, fetchNextPage],
);
useScrollThreshold(handleInfiniteScroll, 50);
const allAgents =
agents?.pages.flatMap((page) => {
const data = page.data as LibraryAgentResponse;
return data.agents;
}) ?? [];
return {
allAgents,
agentLoading,
isFetchingNextPage,
hasNextPage,
isSearching: isFetching && !isFetchingNextPage,
};
};

View File

@@ -11,9 +11,9 @@ import {
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import LibraryNotificationCard, {
import NotificationCard, {
NotificationCardData,
} from "./library-notification-card";
} from "../LibraryNotificationCard/LibraryNotificationCard";
export default function LibraryNotificationDropdown(): React.ReactNode {
const controls = useAnimationControls();
@@ -109,7 +109,7 @@ export default function LibraryNotificationDropdown(): React.ReactNode {
{notifications && notifications.length ? (
notifications.map((notification) => (
<DropdownMenuItem key={notification.id} className="p-0">
<LibraryNotificationCard
<NotificationCard
notification={notification}
onClose={() =>
setNotifications((prev) => {

View File

@@ -0,0 +1,38 @@
"use client";
import { Input } from "@/components/ui/input";
import { Search, X } from "lucide-react";
import { useLibrarySearchbar } from "./useLibrarySearchbar";
export default function LibrarySearchBar(): React.ReactNode {
const { handleSearchInput, handleClear, setIsFocused, isFocused, inputRef } =
useLibrarySearchbar();
return (
<div
onClick={() => inputRef.current?.focus()}
className="relative z-[21] mx-auto flex h-[50px] w-full max-w-[500px] flex-1 cursor-pointer items-center rounded-[45px] bg-[#EDEDED] px-[24px] py-[10px]"
>
<Search
className="mr-2 h-[29px] w-[29px] text-neutral-900"
strokeWidth={1.25}
/>
<Input
ref={inputRef}
onFocus={() => setIsFocused(true)}
onBlur={() => !inputRef.current?.value && setIsFocused(false)}
onChange={handleSearchInput}
className="flex-1 border-none font-sans text-[16px] font-normal leading-7 shadow-none focus:shadow-none focus:ring-0"
type="text"
placeholder="Search agents"
/>
{isFocused && inputRef.current?.value && (
<X
className="ml-2 h-[29px] w-[29px] cursor-pointer text-neutral-900"
strokeWidth={1.25}
onClick={handleClear}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,36 @@
import { useRef, useState } from "react";
import { useLibraryPageContext } from "../state-provider";
import { debounce } from "lodash";
export const useLibrarySearchbar = () => {
const inputRef = useRef<HTMLInputElement>(null);
const [isFocused, setIsFocused] = useState(false);
const { setSearchTerm } = useLibraryPageContext();
const debouncedSearch = debounce((value: string) => {
setSearchTerm(value);
}, 300);
const handleSearchInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const searchTerm = e.target.value;
debouncedSearch(searchTerm);
};
const handleClear = (e: React.MouseEvent) => {
if (inputRef.current) {
inputRef.current.value = "";
inputRef.current.blur();
setSearchTerm("");
e.preventDefault();
}
setIsFocused(false);
};
return {
handleClear,
handleSearchInput,
isFocused,
inputRef,
setIsFocused,
};
};

View File

@@ -1,6 +1,4 @@
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { LibraryAgentSortEnum } from "@/lib/autogpt-server-api/types";
import { useLibraryPageContext } from "@/app/(platform)/library/state-provider";
"use client";
import { ArrowDownNarrowWideIcon } from "lucide-react";
import {
Select,
@@ -10,24 +8,11 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { LibraryAgentSort } from "@/app/api/__generated__/models/libraryAgentSort";
import { useLibrarySortMenu } from "./useLibrarySortMenu";
export default function LibrarySortMenu(): React.ReactNode {
const api = useBackendAPI();
const { setAgentLoading, setAgents, setLibrarySort, searchTerm } =
useLibraryPageContext();
const handleSortChange = async (value: LibraryAgentSortEnum) => {
setLibrarySort(value);
setAgentLoading(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
const response = await api.listLibraryAgents({
search_term: searchTerm,
sort_by: value,
page: 1,
});
setAgents(response.agents);
setAgentLoading(false);
};
const { handleSortChange } = useLibrarySortMenu();
return (
<div className="flex items-center">
<span className="hidden whitespace-nowrap sm:inline">sort by</span>
@@ -38,10 +23,10 @@ export default function LibrarySortMenu(): React.ReactNode {
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value={LibraryAgentSortEnum.CREATED_AT}>
<SelectItem value={LibraryAgentSort.createdAt}>
Creation Date
</SelectItem>
<SelectItem value={LibraryAgentSortEnum.UPDATED_AT}>
<SelectItem value={LibraryAgentSort.updatedAt}>
Last Modified
</SelectItem>
</SelectGroup>

View File

@@ -0,0 +1,27 @@
import { LibraryAgentSort } from "@/app/api/__generated__/models/libraryAgentSort";
import { useLibraryPageContext } from "../state-provider";
export const useLibrarySortMenu = () => {
const { setLibrarySort } = useLibraryPageContext();
const handleSortChange = (value: LibraryAgentSort) => {
// Simply updating the sort state - React Query will handle the rest
setLibrarySort(value);
};
const getSortLabel = (sort: LibraryAgentSort) => {
switch (sort) {
case LibraryAgentSort.createdAt:
return "Creation Date";
case LibraryAgentSort.updatedAt:
return "Last Modified";
default:
return "Last Modified";
}
};
return {
handleSortChange,
getSortLabel,
};
};

View File

@@ -1,5 +1,4 @@
"use client";
import { useState } from "react";
import { Upload, X } from "lucide-react";
import { Button } from "@/components/agptui/Button";
import {
@@ -10,8 +9,6 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { FileUploader } from "react-drag-drop-files";
import {
Form,
@@ -23,13 +20,7 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Graph,
GraphCreatable,
sanitizeImportedGraph,
} from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { useToast } from "@/components/ui/use-toast";
import { useLibraryUploadAgentDialog } from "./useLibraryUploadAgentDialog";
const fileTypes = ["JSON"];
@@ -37,98 +28,24 @@ const fileSchema = z.custom<File>((val) => val instanceof File, {
message: "Must be a File object",
});
const formSchema = z.object({
export const uploadAgentFormSchema = z.object({
agentFile: fileSchema,
agentName: z.string().min(1, "Agent name is required"),
agentDescription: z.string(),
});
export default function LibraryUploadAgentDialog(): React.ReactNode {
const [isDroped, setisDroped] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const api = useBackendAPI();
const { toast } = useToast();
const [agentObject, setAgentObject] = useState<GraphCreatable | null>(null);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
agentName: "",
agentDescription: "",
},
});
const onSubmit = async (values: z.infer<typeof formSchema>) => {
if (!agentObject) {
form.setError("root", { message: "No Agent object to save" });
return;
}
setIsLoading(true);
const payload: GraphCreatable = {
...agentObject,
name: values.agentName,
description: values.agentDescription,
is_active: true,
};
try {
const response = await api.createGraph(payload);
setIsOpen(false);
toast({
title: "Success",
description: "Agent uploaded successfully",
variant: "default",
});
const qID = "flowID";
window.location.href = `/build?${qID}=${response.id}`;
} catch (error) {
form.setError("root", {
message: `Could not create agent: ${error}`,
});
} finally {
setIsLoading(false);
}
};
const handleChange = (file: File) => {
setTimeout(() => {
setisDroped(false);
}, 2000);
form.setValue("agentFile", file);
const reader = new FileReader();
reader.onload = (event) => {
try {
const obj = JSON.parse(event.target?.result as string);
if (
!["name", "description", "nodes", "links"].every(
(key) => key in obj && obj[key] != null,
)
) {
throw new Error(
"Invalid agent object in file: " + JSON.stringify(obj, null, 2),
);
}
const agent = obj as Graph;
sanitizeImportedGraph(agent);
setAgentObject(agent);
if (!form.getValues("agentName")) {
form.setValue("agentName", agent.name);
}
if (!form.getValues("agentDescription")) {
form.setValue("agentDescription", agent.description);
}
} catch (error) {
console.error("Error loading agent file:", error);
}
};
reader.readAsText(file);
setisDroped(false);
};
const {
onSubmit,
isUploading,
isOpen,
setIsOpen,
isDroped,
handleChange,
form,
setisDroped,
agentObject,
} = useLibraryUploadAgentDialog();
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
@@ -249,9 +166,9 @@ export default function LibraryUploadAgentDialog(): React.ReactNode {
type="submit"
variant="primary"
className="mt-2 self-end"
disabled={!agentObject || isLoading}
disabled={!agentObject || isUploading}
>
{isLoading ? (
{isUploading ? (
<div className="flex items-center gap-2">
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-t-2 border-white"></div>
<span>Uploading...</span>

View File

@@ -0,0 +1,116 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { uploadAgentFormSchema } from "./LibraryUploadAgentDialog";
import { usePostV1CreateNewGraph } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
import { useToast } from "@/components/ui/use-toast";
import { useState } from "react";
import { Graph } from "@/app/api/__generated__/models/graph";
import { sanitizeImportedGraph } from "@/lib/autogpt-server-api";
export const useLibraryUploadAgentDialog = () => {
const [isDroped, setisDroped] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const { toast } = useToast();
const [agentObject, setAgentObject] = useState<Graph | null>(null);
const { mutateAsync: createGraph, isPending: isUploading } =
usePostV1CreateNewGraph({
mutation: {
onSuccess: ({ data }) => {
setIsOpen(false);
toast({
title: "Success",
description: "Agent uploaded successfully",
variant: "default",
});
const qID = "flowID";
window.location.href = `/build?${qID}=${(data as GraphModel).id}`;
},
onError: () => {
toast({
title: "Error",
description: "Error Uploading agent",
variant: "destructive",
});
},
},
});
const form = useForm<z.infer<typeof uploadAgentFormSchema>>({
resolver: zodResolver(uploadAgentFormSchema),
defaultValues: {
agentName: "",
agentDescription: "",
},
});
const onSubmit = async (values: z.infer<typeof uploadAgentFormSchema>) => {
if (!agentObject) {
form.setError("root", { message: "No Agent object to save" });
return;
}
const payload: Graph = {
...agentObject,
name: values.agentName,
description: values.agentDescription,
is_active: true,
};
await createGraph({
data: {
graph: payload,
},
});
};
const handleChange = (file: File) => {
setTimeout(() => {
setisDroped(false);
}, 2000);
form.setValue("agentFile", file);
const reader = new FileReader();
reader.onload = (event) => {
try {
const obj = JSON.parse(event.target?.result as string);
if (
!["name", "description", "nodes", "links"].every(
(key) => key in obj && obj[key] != null,
)
) {
throw new Error(
"Invalid agent object in file: " + JSON.stringify(obj, null, 2),
);
}
const agent = obj as Graph;
sanitizeImportedGraph(agent);
setAgentObject(agent);
if (!form.getValues("agentName")) {
form.setValue("agentName", agent.name);
}
if (!form.getValues("agentDescription")) {
form.setValue("agentDescription", agent.description);
}
} catch (error) {
console.error("Error loading agent file:", error);
}
};
reader.readAsText(file);
setisDroped(false);
};
return {
onSubmit,
isUploading,
isOpen,
setIsOpen,
form,
agentObject,
isDroped,
handleChange,
setisDroped,
};
};

View File

@@ -1,5 +1,6 @@
"use client";
import { LibraryAgentSort } from "@/app/api/__generated__/models/libraryAgentSort";
import {
createContext,
useState,
@@ -8,19 +9,14 @@ import {
Dispatch,
SetStateAction,
} from "react";
import { LibraryAgent, LibraryAgentSortEnum } from "@/lib/autogpt-server-api";
interface LibraryPageContextType {
agents: LibraryAgent[];
setAgents: Dispatch<SetStateAction<LibraryAgent[]>>;
agentLoading: boolean;
setAgentLoading: Dispatch<SetStateAction<boolean>>;
searchTerm: string | undefined;
setSearchTerm: Dispatch<SetStateAction<string | undefined>>;
searchTerm: string;
setSearchTerm: Dispatch<SetStateAction<string>>;
uploadedFile: File | null;
setUploadedFile: Dispatch<SetStateAction<File | null>>;
librarySort: LibraryAgentSortEnum;
setLibrarySort: Dispatch<SetStateAction<LibraryAgentSortEnum>>;
librarySort: LibraryAgentSort;
setLibrarySort: Dispatch<SetStateAction<LibraryAgentSort>>;
}
export const LibraryPageContext = createContext<LibraryPageContextType>(
@@ -32,21 +28,15 @@ export function LibraryPageStateProvider({
}: {
children: ReactNode;
}) {
const [agents, setAgents] = useState<LibraryAgent[]>([]);
const [agentLoading, setAgentLoading] = useState<boolean>(true);
const [searchTerm, setSearchTerm] = useState<string | undefined>("");
const [searchTerm, setSearchTerm] = useState<string>("");
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
const [librarySort, setLibrarySort] = useState<LibraryAgentSortEnum>(
LibraryAgentSortEnum.UPDATED_AT,
const [librarySort, setLibrarySort] = useState<LibraryAgentSort>(
LibraryAgentSort.updatedAt,
);
return (
<LibraryPageContext.Provider
value={{
agents,
setAgents,
agentLoading,
setAgentLoading,
searchTerm,
setSearchTerm,
uploadedFile,

View File

@@ -1,5 +1,5 @@
"use client";
import Link from "next/link";
import { Metadata } from "next/types";
import {
ArrowBottomRightIcon,
@@ -7,15 +7,9 @@ import {
} from "@radix-ui/react-icons";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { LibraryPageStateProvider } from "./state-provider";
import LibraryActionSubHeader from "@/components/library/library-action-sub-header";
import LibraryActionHeader from "@/components/library/library-action-header";
import LibraryAgentList from "@/components/library/library-agent-list";
export const metadata: Metadata = {
title: "Library - AutoGPT Platform",
description: "Your collection of Agents on the AutoGPT Platform",
};
import { LibraryPageStateProvider } from "./components/state-provider";
import LibraryActionHeader from "./components/LibraryActionHeader/LibraryActionHeader";
import LibraryAgentList from "./components/LibraryAgentList/LibraryAgentList";
/**
* LibraryPage Component
@@ -25,13 +19,7 @@ export default function LibraryPage() {
return (
<main className="container min-h-screen space-y-4 pb-20 sm:px-8 md:px-12">
<LibraryPageStateProvider>
{/* Header section containing notifications, search functionality and upload mechanism */}
<LibraryActionHeader />
{/* Subheader section containing agent counts and filtering options */}
<LibraryActionSubHeader />
{/* Content section displaying agent list with counter and filtering options */}
<LibraryAgentList />
</LibraryPageStateProvider>

File diff suppressed because it is too large Load Diff

View File

@@ -1,95 +0,0 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { useLibraryPageContext } from "@/app/(platform)/library/state-provider";
import { useScrollThreshold } from "@/hooks/useScrollThreshold";
import LibraryAgentCard from "./library-agent-card";
/**
* Displays a grid of library agents with infinite scroll functionality.
*/
export default function LibraryAgentList(): React.ReactNode {
const [currentPage, setCurrentPage] = useState<number>(1);
const [loadingMore, setLoadingMore] = useState(false);
const [hasMore, setHasMore] = useState(true);
const api = useBackendAPI();
const { agents, setAgents, setAgentLoading, agentLoading } =
useLibraryPageContext();
const fetchAgents = useCallback(
async (page: number) => {
try {
const response = await api.listLibraryAgents(
page === 1 ? {} : { page: page },
);
if (page > 1) {
setAgents((prevAgent) => [...prevAgent, ...response.agents]);
} else {
setAgents(response.agents);
}
setHasMore(
response.pagination.current_page * response.pagination.page_size <
response.pagination.total_items,
);
} finally {
setAgentLoading(false);
setLoadingMore(false);
}
},
[api, setAgents, setAgentLoading],
);
useEffect(() => {
fetchAgents(1);
}, [fetchAgents]);
const handleInfiniteScroll = useCallback(
(scrollY: number) => {
if (!hasMore || loadingMore) return;
const { scrollHeight, clientHeight } = document.documentElement;
const SCROLL_THRESHOLD = 20;
const FETCH_DELAY = 1000;
if (scrollY + clientHeight >= scrollHeight - SCROLL_THRESHOLD) {
setLoadingMore(true);
const nextPage = currentPage + 1;
setCurrentPage(nextPage);
setTimeout(() => fetchAgents(nextPage), FETCH_DELAY);
}
},
[currentPage, hasMore, loadingMore, fetchAgents],
);
useScrollThreshold(handleInfiniteScroll, 50);
const LoadingSpinner = () => (
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-t-2 border-neutral-800" />
);
return (
<div className="px-2">
{agentLoading ? (
<div className="flex h-[200px] items-center justify-center">
<LoadingSpinner />
</div>
) : (
<>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{agents.map((agent) => (
<LibraryAgentCard key={agent.id} agent={agent} />
))}
</div>
{loadingMore && hasMore && (
<div className="flex items-center justify-center py-4 pt-8">
<LoadingSpinner />
</div>
)}
</>
)}
</div>
);
}

View File

@@ -1,74 +0,0 @@
"use client";
import { useRef, useState } from "react";
import debounce from "lodash/debounce";
import { Input } from "@/components/ui/input";
import { Search, X } from "lucide-react";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { useLibraryPageContext } from "@/app/(platform)/library/state-provider";
export default function LibrarySearchBar(): React.ReactNode {
const inputRef = useRef<HTMLInputElement>(null);
const [isFocused, setIsFocused] = useState(false);
const api = useBackendAPI();
const { setAgentLoading, setAgents, librarySort, setSearchTerm } =
useLibraryPageContext();
const debouncedSearch = debounce(async (value: string) => {
try {
setAgentLoading(true);
setSearchTerm(value);
await new Promise((resolve) => setTimeout(resolve, 1000));
const response = await api.listLibraryAgents({
search_term: value,
sort_by: librarySort,
page: 1,
});
setAgents(response.agents);
setAgentLoading(false);
} catch (error) {
console.error("Search failed:", error);
}
}, 300);
const handleSearchInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const searchTerm = e.target.value;
debouncedSearch(searchTerm);
};
return (
<div
onClick={() => inputRef.current?.focus()}
className="relative z-[21] mx-auto flex h-[50px] w-full max-w-[500px] flex-1 cursor-pointer items-center rounded-[45px] bg-[#EDEDED] px-[24px] py-[10px]"
>
<Search
className="mr-2 h-[29px] w-[29px] text-neutral-900"
strokeWidth={1.25}
/>
<Input
ref={inputRef}
onFocus={() => setIsFocused(true)}
onBlur={() => !inputRef.current?.value && setIsFocused(false)}
onChange={handleSearchInput}
className="flex-1 border-none font-sans text-[16px] font-normal leading-7 shadow-none focus:shadow-none focus:ring-0"
type="text"
placeholder="Search agents"
/>
{isFocused && inputRef.current?.value && (
<X
className="ml-2 h-[29px] w-[29px] cursor-pointer text-neutral-900"
strokeWidth={1.25}
onClick={(e: React.MouseEvent) => {
if (inputRef.current) {
debouncedSearch("");
inputRef.current.value = "";
inputRef.current.blur();
e.preventDefault();
}
setIsFocused(false);
}}
/>
)}
</div>
);
}

View File

@@ -1,14 +1,15 @@
import { Connection } from "@xyflow/react";
import { Graph, Block, BlockUIType, Link } from "./types";
import { Block, BlockUIType, Link } from "./types";
import { Graph } from "@/app/api/__generated__/models/graph";
export function removeAgentInputBlockValues(graph: Graph, blocks: Block[]) {
const inputBlocks = graph.nodes.filter(
const inputBlocks = graph.nodes?.filter(
(node) =>
blocks.find((b) => b.id === node.block_id)?.uiType === BlockUIType.INPUT,
);
const modifiedNodes = graph.nodes.map((node) => {
if (inputBlocks.find((inputNode) => inputNode.id === node.id)) {
const modifiedNodes = graph.nodes?.map((node) => {
if (inputBlocks?.find((inputNode) => inputNode.id === node.id)) {
return {
...node,
input_default: {
@@ -61,7 +62,7 @@ function removeCredentials(obj: any): void {
*/
function updateBlockIDs(graph: Graph) {
graph.nodes
.filter((node) => node.block_id in updatedBlockIDMap)
?.filter((node) => node.block_id in updatedBlockIDMap)
.forEach((node) => {
node.block_id = updatedBlockIDMap[node.block_id];
});