mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-30 03:00:41 -04:00
Merge 'dev' into 'cursor/update-login-and-signup-pages-931a'
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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 />
|
||||
@@ -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
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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) => {
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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];
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user