@@ -13,7 +16,7 @@ export default function LibraryActionSubHeader(): React.ReactNode {
My agents
- {agents.length} agents
+ {agentCount} agents
diff --git a/autogpt_platform/frontend/src/components/library/library-agent-card.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/LibraryAgentCard.tsx
similarity index 95%
rename from autogpt_platform/frontend/src/components/library/library-agent-card.tsx
rename to autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/LibraryAgentCard.tsx
index a2ee2ccc56..c9d21d5629 100644
--- a/autogpt_platform/frontend/src/components/library/library-agent-card.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentCard/LibraryAgentCard.tsx
@@ -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 (
(
+
+ );
+
+ return (
+ <>
+ {/* TODO: We need a new endpoint on backend that returns total number of agents */}
+
+
+ {agentLoading ? (
+
+
+
+ ) : (
+ <>
+
+ {agents.map((agent) => (
+
+ ))}
+
+ {(isFetchingNextPage || isSearching) && (
+
+
+
+ )}
+ >
+ )}
+
+ >
+ );
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/useLibraryAgentList.ts b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/useLibraryAgentList.ts
new file mode 100644
index 0000000000..d5d9e41933
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryAgentList/useLibraryAgentList.ts
@@ -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,
+ };
+};
diff --git a/autogpt_platform/frontend/src/components/library/library-notification-card.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryNotificationCard/LibraryNotificationCard.tsx
similarity index 100%
rename from autogpt_platform/frontend/src/components/library/library-notification-card.tsx
rename to autogpt_platform/frontend/src/app/(platform)/library/components/LibraryNotificationCard/LibraryNotificationCard.tsx
diff --git a/autogpt_platform/frontend/src/components/library/library-notification-dropdown.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryNotificationDropdown/LibraryNotificationDropdown.tsx
similarity index 97%
rename from autogpt_platform/frontend/src/components/library/library-notification-dropdown.tsx
rename to autogpt_platform/frontend/src/app/(platform)/library/components/LibraryNotificationDropdown/LibraryNotificationDropdown.tsx
index 8bb131ceb4..f38ade2359 100644
--- a/autogpt_platform/frontend/src/components/library/library-notification-dropdown.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryNotificationDropdown/LibraryNotificationDropdown.tsx
@@ -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) => (
-
setNotifications((prev) => {
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/LibrarySearchBar.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/LibrarySearchBar.tsx
new file mode 100644
index 0000000000..099f036052
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/LibrarySearchBar.tsx
@@ -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 (
+ 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]"
+ >
+
+
+ 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 && (
+
+ )}
+
+ );
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/useLibrarySearchbar.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/useLibrarySearchbar.tsx
new file mode 100644
index 0000000000..f6428c6c4e
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/useLibrarySearchbar.tsx
@@ -0,0 +1,36 @@
+import { useRef, useState } from "react";
+import { useLibraryPageContext } from "../state-provider";
+import { debounce } from "lodash";
+
+export const useLibrarySearchbar = () => {
+ const inputRef = useRef(null);
+ const [isFocused, setIsFocused] = useState(false);
+ const { setSearchTerm } = useLibraryPageContext();
+
+ const debouncedSearch = debounce((value: string) => {
+ setSearchTerm(value);
+ }, 300);
+
+ const handleSearchInput = (e: React.ChangeEvent) => {
+ 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,
+ };
+};
diff --git a/autogpt_platform/frontend/src/components/library/library-sort-menu.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySortMenu/LibrarySortMenu.tsx
similarity index 52%
rename from autogpt_platform/frontend/src/components/library/library-sort-menu.tsx
rename to autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySortMenu/LibrarySortMenu.tsx
index c5ac2e819c..d1d973d295 100644
--- a/autogpt_platform/frontend/src/components/library/library-sort-menu.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySortMenu/LibrarySortMenu.tsx
@@ -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 (
sort by
@@ -38,10 +23,10 @@ export default function LibrarySortMenu(): React.ReactNode {
-
+
Creation Date
-
+
Last Modified
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySortMenu/useLibrarySortMenu.ts b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySortMenu/useLibrarySortMenu.ts
new file mode 100644
index 0000000000..d2575c8936
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySortMenu/useLibrarySortMenu.ts
@@ -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,
+ };
+};
diff --git a/autogpt_platform/frontend/src/components/library/library-upload-agent-dialog.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryUploadAgentDialog/LibraryUploadAgentDialog.tsx
similarity index 68%
rename from autogpt_platform/frontend/src/components/library/library-upload-agent-dialog.tsx
rename to autogpt_platform/frontend/src/app/(platform)/library/components/LibraryUploadAgentDialog/LibraryUploadAgentDialog.tsx
index a032f23361..e095da4f5f 100644
--- a/autogpt_platform/frontend/src/components/library/library-upload-agent-dialog.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibraryUploadAgentDialog/LibraryUploadAgentDialog.tsx
@@ -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((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(null);
-
- const form = useForm>({
- resolver: zodResolver(formSchema),
- defaultValues: {
- agentName: "",
- agentDescription: "",
- },
- });
-
- const onSubmit = async (values: z.infer) => {
- 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 (