mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-19 20:18:22 -05:00
Compare commits
6 Commits
fix/undefi
...
abhi/open-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03a70d8a27 | ||
|
|
dc72045515 | ||
|
|
97311195d7 | ||
|
|
bf63361349 | ||
|
|
f60b58032f | ||
|
|
5504680cc7 |
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
from typing import List
|
||||
import typing
|
||||
|
||||
import prisma.errors
|
||||
import prisma.models
|
||||
@@ -15,17 +16,21 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
async def get_library_agents(
|
||||
user_id: str,
|
||||
limit: int = 20,
|
||||
offset: int = 0,
|
||||
) -> List[backend.server.v2.library.model.LibraryAgent]:
|
||||
"""
|
||||
Returns all agents (AgentGraph) that belong to the user and all agents in their library (UserAgent table)
|
||||
Returns paginated agents (AgentGraph) that belong to the user and agents in their library (UserAgent table)
|
||||
"""
|
||||
logger.debug(f"Getting library agents for user {user_id}")
|
||||
logger.debug(f"Getting library agents for user {user_id} with limit {limit} offset {offset}")
|
||||
|
||||
try:
|
||||
# Get agents created by user with nodes and links
|
||||
user_created = await prisma.models.AgentGraph.prisma().find_many(
|
||||
where=prisma.types.AgentGraphWhereInput(userId=user_id, isActive=True),
|
||||
include=backend.data.includes.AGENT_GRAPH_INCLUDE,
|
||||
skip=offset,
|
||||
take=limit,
|
||||
)
|
||||
|
||||
# Get agents in user's library with nodes and links
|
||||
@@ -47,6 +52,8 @@ async def get_library_agents(
|
||||
}
|
||||
}
|
||||
},
|
||||
skip=offset,
|
||||
take=limit,
|
||||
)
|
||||
|
||||
# Convert to Graph models first
|
||||
@@ -94,6 +101,131 @@ async def get_library_agents(
|
||||
"Failed to fetch library agents"
|
||||
) from e
|
||||
|
||||
async def search_library_agents(
|
||||
user_id: str,
|
||||
search_term: str,
|
||||
sort_by: backend.server.v2.library.model.LibraryAgentFilter,
|
||||
limit: int = 20,
|
||||
offset: int = 0,
|
||||
) -> List[backend.server.v2.library.model.LibraryAgent]:
|
||||
"""
|
||||
Searches paginated agents (AgentGraph) that belong to the user and agents in their library (UserAgent table)
|
||||
based on name or description containing the search term
|
||||
"""
|
||||
logger.debug(f"Searching library agents for user {user_id} with term '{search_term}', limit {limit} offset {offset}")
|
||||
|
||||
try:
|
||||
# Get user created agents matching search
|
||||
user_created = await prisma.models.AgentGraph.prisma().find_many(
|
||||
where=prisma.types.AgentGraphWhereInput(
|
||||
userId=user_id,
|
||||
isActive=True,
|
||||
OR=[
|
||||
{"name": {"contains": search_term, "mode": "insensitive"}},
|
||||
{"description": {"contains": search_term, "mode": "insensitive"}}
|
||||
]
|
||||
),
|
||||
include=backend.data.includes.AGENT_GRAPH_INCLUDE,
|
||||
skip=offset,
|
||||
take=limit,
|
||||
)
|
||||
|
||||
# Get library agents matching search
|
||||
# Only applying filters on UserAgent table
|
||||
order_by : prisma.types.UserAgentOrderByInput = {"updatedAt": "desc"}
|
||||
where = prisma.types.UserAgentWhereInput(
|
||||
userId=user_id,
|
||||
isDeleted=False,
|
||||
isArchived=False,
|
||||
Agent={
|
||||
"is": {
|
||||
"OR": [
|
||||
{"name": {"contains": search_term, "mode": "insensitive"}},
|
||||
{"description": {"contains": search_term, "mode": "insensitive"}}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
if sort_by == backend.server.v2.library.model.LibraryAgentFilter.UPDATED_AT.value:
|
||||
order_by = {"updatedAt": "desc"}
|
||||
|
||||
elif sort_by == backend.server.v2.library.model.LibraryAgentFilter.CREATED_AT.value:
|
||||
order_by = {"createdAt": "desc"}
|
||||
|
||||
elif sort_by == backend.server.v2.library.model.LibraryAgentFilter.IS_FAVOURITE.value:
|
||||
where["isFavorite"] = True
|
||||
order_by = {"updatedAt": "desc"}
|
||||
|
||||
elif sort_by == backend.server.v2.library.model.LibraryAgentFilter.IS_CREATED_BY_USER.value:
|
||||
where["isCreatedByUser"] = True
|
||||
order_by = {"updatedAt": "desc"}
|
||||
|
||||
|
||||
library_agents = await prisma.models.UserAgent.prisma().find_many(
|
||||
where=where,
|
||||
include={
|
||||
"Agent": {
|
||||
"include": {
|
||||
"AgentNodes": {
|
||||
"include": {
|
||||
"Input": True,
|
||||
"Output": True,
|
||||
"Webhook": True,
|
||||
"AgentBlock": True,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
skip=offset,
|
||||
order = order_by,
|
||||
take=limit,
|
||||
)
|
||||
|
||||
# Convert to Graph models
|
||||
graphs = []
|
||||
|
||||
for agent in user_created:
|
||||
try:
|
||||
graphs.append(backend.data.graph.GraphModel.from_db(agent))
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing searched user agent {agent.id}: {e}")
|
||||
continue
|
||||
|
||||
for agent in library_agents:
|
||||
if agent.Agent:
|
||||
try:
|
||||
graphs.append(backend.data.graph.GraphModel.from_db(agent.Agent))
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing searched library agent {agent.agentId}: {e}")
|
||||
continue
|
||||
|
||||
# Convert to LibraryAgent models
|
||||
result = []
|
||||
for graph in graphs:
|
||||
result.append(
|
||||
backend.server.v2.library.model.LibraryAgent(
|
||||
id=graph.id,
|
||||
version=graph.version,
|
||||
is_active=graph.is_active,
|
||||
name=graph.name,
|
||||
description=graph.description,
|
||||
isCreatedByUser=any(a.id == graph.id for a in user_created),
|
||||
input_schema=graph.input_schema,
|
||||
output_schema=graph.output_schema,
|
||||
)
|
||||
)
|
||||
|
||||
logger.debug(f"Found {len(result)} library agents matching search")
|
||||
return result
|
||||
|
||||
except prisma.errors.PrismaError as e:
|
||||
logger.error(f"Database error searching library agents: {str(e)}")
|
||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Failed to search library agents"
|
||||
) from e
|
||||
|
||||
async def add_agent_to_library(store_listing_version_id: str, user_id: str) -> None:
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from enum import Enum
|
||||
import typing
|
||||
|
||||
import pydantic
|
||||
@@ -14,3 +15,9 @@ class LibraryAgent(pydantic.BaseModel):
|
||||
# Made input_schema and output_schema match GraphMeta's type
|
||||
input_schema: dict[str, typing.Any] # Should be BlockIOObjectSubSchema in frontend
|
||||
output_schema: dict[str, typing.Any] # Should be BlockIOObjectSubSchema in frontend
|
||||
|
||||
class LibraryAgentFilter(str, Enum):
|
||||
CREATED_AT = "createdAt"
|
||||
UPDATED_AT = "updatedAt"
|
||||
IS_FAVOURITE = "isFavourite"
|
||||
IS_CREATED_BY_USER = "isCreatedByUser"
|
||||
|
||||
@@ -4,7 +4,6 @@ import typing
|
||||
import autogpt_libs.auth.depends
|
||||
import autogpt_libs.auth.middleware
|
||||
import fastapi
|
||||
import prisma
|
||||
|
||||
import backend.data.graph
|
||||
import backend.integrations.creds_manager
|
||||
@@ -19,7 +18,7 @@ integration_creds_manager = (
|
||||
backend.integrations.creds_manager.IntegrationCredentialsManager()
|
||||
)
|
||||
|
||||
|
||||
# ✅
|
||||
@router.get(
|
||||
"/agents",
|
||||
tags=["library", "private"],
|
||||
@@ -28,14 +27,37 @@ integration_creds_manager = (
|
||||
async def get_library_agents(
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
]
|
||||
) -> typing.Sequence[backend.server.v2.library.model.LibraryAgent]:
|
||||
],
|
||||
pagination_token: str | None = fastapi.Query(None)
|
||||
) -> dict[str, typing.Any]:
|
||||
"""
|
||||
Get all agents in the user's library, including both created and saved agents.
|
||||
Get agents in the user's library with pagination (20 agents per page).
|
||||
|
||||
Args:
|
||||
user_id: ID of the authenticated user
|
||||
pagination_token: Token to get next page of results
|
||||
|
||||
Returns:
|
||||
Dictionary containing:
|
||||
- agents: List of agents for current page
|
||||
- next_token: Token to get next page (None if no more pages)
|
||||
"""
|
||||
try:
|
||||
agents = await backend.server.v2.library.db.get_library_agents(user_id)
|
||||
return agents
|
||||
page_size = 20
|
||||
agents = await backend.server.v2.library.db.get_library_agents(
|
||||
user_id,
|
||||
limit=page_size + 1,
|
||||
offset=int(pagination_token) if pagination_token else 0
|
||||
)
|
||||
|
||||
has_more = len(agents) > page_size
|
||||
agents = agents[:page_size]
|
||||
next_token = str(int(pagination_token or 0) + page_size) if has_more else None
|
||||
|
||||
return {
|
||||
"agents": agents,
|
||||
"next_token": next_token
|
||||
}
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting library agents")
|
||||
raise fastapi.HTTPException(
|
||||
@@ -43,6 +65,58 @@ async def get_library_agents(
|
||||
)
|
||||
|
||||
|
||||
# For searching and filtering the library agents
|
||||
@router.get(
|
||||
"/agents/search",
|
||||
tags=["library", "private"],
|
||||
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
|
||||
)
|
||||
async def search_library_agents(
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
],
|
||||
search_term: str = fastapi.Query(..., description="Search term to filter agents"),
|
||||
sort_by: backend.server.v2.library.model.LibraryAgentFilter = fastapi.Query(backend.server.v2.library.model.LibraryAgentFilter.UPDATED_AT, description="Sort results by criteria"),
|
||||
pagination_token: str | None = fastapi.Query(None)
|
||||
) -> dict[str, typing.Any]:
|
||||
"""
|
||||
Search for agents in the user's library with pagination (20 agents per page).
|
||||
|
||||
Args:
|
||||
user_id: ID of the authenticated user
|
||||
search_term: Term to search for in agent names/descriptions
|
||||
sort_by: How to sort results (most_recent, highest_runtime, most_runs, alphabetical, last_modified)
|
||||
pagination_token: Token to get next page of results
|
||||
|
||||
Returns:
|
||||
Dictionary containing:
|
||||
- agents: List of matching agents for current page
|
||||
- next_token: Token to get next page (None if no more pages)
|
||||
"""
|
||||
try:
|
||||
page_size = 20
|
||||
agents = await backend.server.v2.library.db.search_library_agents(
|
||||
user_id,
|
||||
search_term,
|
||||
sort_by=sort_by,
|
||||
limit=page_size + 1,
|
||||
offset=int(pagination_token) if pagination_token else 0
|
||||
)
|
||||
|
||||
has_more = len(agents) > page_size
|
||||
agents = agents[:page_size]
|
||||
next_token = str(int(pagination_token or 0) + page_size) if has_more else None
|
||||
|
||||
return {
|
||||
"agents": agents,
|
||||
"next_token": next_token
|
||||
}
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst searching library agents")
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail="Failed to search library agents"
|
||||
)
|
||||
|
||||
@router.post(
|
||||
"/agents/{store_listing_version_id}",
|
||||
tags=["library", "private"],
|
||||
@@ -70,49 +144,51 @@ async def add_agent_to_library(
|
||||
"""
|
||||
try:
|
||||
# Get the graph from the store listing
|
||||
store_listing_version = (
|
||||
await prisma.models.StoreListingVersion.prisma().find_unique(
|
||||
where={"id": store_listing_version_id}, include={"Agent": True}
|
||||
)
|
||||
)
|
||||
# store_listing_version = (
|
||||
# await prisma.models.StoreListingVersion.prisma().find_unique(
|
||||
# where={"id": store_listing_version_id}, include={"Agent": True}
|
||||
# )
|
||||
# )
|
||||
|
||||
if not store_listing_version or not store_listing_version.Agent:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Store listing version {store_listing_version_id} not found",
|
||||
)
|
||||
# if not store_listing_version or not store_listing_version.Agent:
|
||||
# raise fastapi.HTTPException(
|
||||
# status_code=404,
|
||||
# detail=f"Store listing version {store_listing_version_id} not found",
|
||||
# )
|
||||
|
||||
agent = store_listing_version.Agent
|
||||
# agent = store_listing_version.Agent
|
||||
|
||||
if agent.userId == user_id:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=400, detail="Cannot add own agent to library"
|
||||
)
|
||||
# if agent.userId == user_id:
|
||||
# raise fastapi.HTTPException(
|
||||
# status_code=400, detail="Cannot add own agent to library"
|
||||
# )
|
||||
|
||||
# Create a new graph from the template
|
||||
graph = await backend.data.graph.get_graph(
|
||||
agent.id, agent.version, user_id=user_id
|
||||
)
|
||||
|
||||
if not graph:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=404, detail=f"Agent {agent.id} not found"
|
||||
)
|
||||
# if not graph:
|
||||
# raise fastapi.HTTPException(
|
||||
# status_code=404, detail=f"Agent {agent.id} not found"
|
||||
# )
|
||||
|
||||
# Create a deep copy with new IDs
|
||||
graph.version = 1
|
||||
graph.is_template = False
|
||||
graph.is_active = True
|
||||
graph.reassign_ids(user_id=user_id, reassign_graph_id=True)
|
||||
# # Create a deep copy with new IDs
|
||||
# graph.version = 1
|
||||
# graph.is_template = False
|
||||
# graph.is_active = True
|
||||
# graph.reassign_ids(user_id=user_id, reassign_graph_id=True)
|
||||
|
||||
# Save the new graph
|
||||
graph = await backend.data.graph.create_graph(graph, user_id=user_id)
|
||||
graph = (
|
||||
await backend.integrations.webhooks.graph_lifecycle_hooks.on_graph_activate(
|
||||
graph,
|
||||
get_credentials=lambda id: integration_creds_manager.get(user_id, id),
|
||||
)
|
||||
)
|
||||
# # Save the new graph
|
||||
# graph = await backend.data.graph.create_graph(graph, user_id=user_id)
|
||||
# graph = (
|
||||
# await backend.integrations.webhooks.graph_lifecycle_hooks.on_graph_activate(
|
||||
# graph,
|
||||
# get_credentials=lambda id: integration_creds_manager.get(user_id, id),
|
||||
# )
|
||||
# )
|
||||
|
||||
await backend.server.v2.library.db.add_agent_to_library(store_listing_version_id=store_listing_version_id,user_id=user_id)
|
||||
|
||||
return fastapi.Response(status_code=201)
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ class Config(UpdateTrackingModel["Config"], BaseSettings):
|
||||
)
|
||||
|
||||
media_gcs_bucket_name: str = Field(
|
||||
default="",
|
||||
default="autogpt_bucket",
|
||||
description="The name of the Google Cloud Storage bucket for media files",
|
||||
)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ const nextConfig = {
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
transpilePackages: ["geist"],
|
||||
};
|
||||
|
||||
export default withSentryConfig(nextConfig, {
|
||||
|
||||
@@ -63,13 +63,15 @@
|
||||
"framer-motion": "^11.16.0",
|
||||
"geist": "^1.3.1",
|
||||
"launchdarkly-react-client-sdk": "^3.6.0",
|
||||
"lucide-react": "^0.474.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lucide-react": "^0.468.0",
|
||||
"moment": "^2.30.1",
|
||||
"next": "^14.2.21",
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "^18",
|
||||
"react-day-picker": "^9.5.1",
|
||||
"react-dom": "^18",
|
||||
"react-drag-drop-files": "^2.4.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-markdown": "^9.0.3",
|
||||
@@ -93,6 +95,7 @@
|
||||
"@storybook/nextjs": "^8.5.2",
|
||||
"@storybook/react": "^8.3.5",
|
||||
"@storybook/test": "^8.3.5",
|
||||
"@types/lodash": "^4.17.13",
|
||||
"@storybook/test-runner": "^0.21.0",
|
||||
"@types/negotiator": "^0.6.3",
|
||||
"@types/node": "^22.10.10",
|
||||
|
||||
8
autogpt_platform/frontend/src/app/agents/layout.tsx
Normal file
8
autogpt_platform/frontend/src/app/agents/layout.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { LibraryPageProvider } from "@/components/agptui/providers/LibraryAgentProvider";
|
||||
import React from "react";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return <LibraryPageProvider>{children}</LibraryPageProvider>;
|
||||
}
|
||||
21
autogpt_platform/frontend/src/app/agents/page.tsx
Normal file
21
autogpt_platform/frontend/src/app/agents/page.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import LibraryActionHeader from "@/components/agptui/composite/LibraryActionHeader";
|
||||
import LibraryAgentListContainer from "@/components/agptui/composite/LibraryAgentListContainer";
|
||||
|
||||
/**
|
||||
* LibraryPage Component
|
||||
* Main component that manages the library interface including agent listing and actions
|
||||
*/
|
||||
|
||||
const LibraryPage = () => {
|
||||
return (
|
||||
<main className="mx-auto w-screen max-w-[1600px] space-y-[16px] bg-neutral-50 p-4 px-2 dark:bg-neutral-900 sm:px-8 md:px-12">
|
||||
{/* Header section containing notifications, search functionality, agent count, filters and upload mechanism */}
|
||||
<LibraryActionHeader />
|
||||
|
||||
{/* Content section displaying agent list with counter and filtering options */}
|
||||
<LibraryAgentListContainer />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default LibraryPage;
|
||||
@@ -120,3 +120,12 @@
|
||||
@apply shadow-sm focus-visible:shadow-md;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-none {
|
||||
scrollbar-width: thin; /* For Firefox (sets a thin scrollbar) */
|
||||
scrollbar-color: transparent transparent; /* For Firefox (thumb and track colors) */
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Inter, Poppins } from "next/font/google";
|
||||
import { Providers } from "@/app/providers";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Navbar } from "@/components/agptui/Navbar";
|
||||
import { GeistSans } from "geist/font/sans";
|
||||
import { GeistMono } from "geist/font/mono";
|
||||
|
||||
import "./globals.css";
|
||||
import TallyPopupSimple from "@/components/TallyPopup";
|
||||
@@ -21,6 +23,12 @@ const poppins = Poppins({
|
||||
variable: "--font-poppins",
|
||||
});
|
||||
|
||||
const poppins = Poppins({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600", "700"],
|
||||
variable: "--font-poppins",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "NextGen AutoGPT",
|
||||
description: "Your one stop shop to creating AI Agents",
|
||||
|
||||
@@ -38,7 +38,7 @@ const Monitor = () => {
|
||||
|
||||
const fetchAgents = useCallback(() => {
|
||||
api.listLibraryAgents().then((agent) => {
|
||||
setFlows(agent);
|
||||
setFlows(agent.agents);
|
||||
});
|
||||
api.getExecutions().then((executions) => {
|
||||
setExecutions(executions);
|
||||
|
||||
@@ -20,6 +20,9 @@ const buttonVariants = cva(
|
||||
ghost:
|
||||
"hover:bg-neutral-100 text-[#272727] dark:text-neutral-100 dark:hover:bg-neutral-700",
|
||||
link: "text-[#272727] underline-offset-4 hover:underline dark:text-neutral-100",
|
||||
library_outline:
|
||||
"rounded-[52px] hover:bg-[#262626] border border-zinc-700 hover:text-white font-sans",
|
||||
library_primary: "rounded-[52px] bg-[#262626] text-white font-sans",
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
@@ -29,6 +32,7 @@ const buttonVariants = cva(
|
||||
primary:
|
||||
"h-10 w-28 sm:h-12 sm:w-32 md:h-[4.375rem] md:w-[11rem] lg:h-[3.125rem] lg:w-[7rem]",
|
||||
icon: "h-10 w-10 sm:h-12 sm:w-12 md:h-14 md:w-14 lg:h-[4.375rem] lg:w-[4.375rem]",
|
||||
library: "py-2 px-[14px] h-10 text-[14px] font-medium leading-6 ",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -48,8 +52,11 @@ export interface ButtonProps
|
||||
| "outline"
|
||||
| "secondary"
|
||||
| "ghost"
|
||||
| "link";
|
||||
size?: "default" | "sm" | "lg" | "primary" | "icon";
|
||||
| "link"
|
||||
| "library_outline"
|
||||
| "library_primary";
|
||||
|
||||
size?: "default" | "sm" | "lg" | "primary" | "icon" | "library";
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { GraphMeta } from "@/lib/autogpt-server-api";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
|
||||
export const LibraryAgentCard = ({ id, name, isCreatedByUser }: GraphMeta) => {
|
||||
const descriptions = `An intelligent agent that helps automate your workflow,
|
||||
saving valuable time and improving productivity with smart automations,
|
||||
and enabling you to focus on what matters most.`;
|
||||
|
||||
const imageUrl = null;
|
||||
return (
|
||||
<div className="inline-flex w-full max-w-[434px] cursor-pointer 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">
|
||||
<div className="relative h-[200px] w-full overflow-hidden rounded-[20px]">
|
||||
{!imageUrl ? (
|
||||
<div
|
||||
className={`h-full w-full ${
|
||||
[
|
||||
"bg-gradient-to-r from-green-200 to-blue-200",
|
||||
"bg-gradient-to-r from-pink-200 to-purple-200",
|
||||
"bg-gradient-to-r from-yellow-200 to-orange-200",
|
||||
"bg-gradient-to-r from-blue-200 to-cyan-200",
|
||||
"bg-gradient-to-r from-indigo-200 to-purple-200",
|
||||
][Math.floor(Math.random() * 5)]
|
||||
}`}
|
||||
style={{
|
||||
backgroundSize: "200% 200%",
|
||||
animation: "gradient 15s ease infinite",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={`${name} preview image`}
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
)}
|
||||
<div className="absolute bottom-4 left-4">
|
||||
<Avatar className="h-16 w-16 border-2 border-white dark:border-gray-800">
|
||||
<AvatarImage
|
||||
src="/avatar-placeholder.png"
|
||||
alt={`${name} creator avatar`}
|
||||
/>
|
||||
<AvatarFallback>{name.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full px-4 py-4">
|
||||
<h3 className="font-poppins mb-2 text-2xl font-semibold leading-tight text-[#272727] dark:text-neutral-100">
|
||||
{name}
|
||||
</h3>
|
||||
|
||||
<p className="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{descriptions}
|
||||
</p>
|
||||
|
||||
<div className="items-between mt-4 flex w-full justify-between">
|
||||
<div className="flex gap-3">
|
||||
<Link
|
||||
href={`/agents/${id}`}
|
||||
className="font-geist text-lg font-semibold text-neutral-800 hover:underline dark:text-neutral-200"
|
||||
>
|
||||
See runs
|
||||
</Link>
|
||||
|
||||
{!isCreatedByUser && (
|
||||
<Link
|
||||
href={`/build?flowID=${id}`}
|
||||
className="font-geist text-lg font-semibold text-neutral-800 hover:underline dark:text-neutral-200"
|
||||
>
|
||||
Open in builder
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { LibraryAgentFilterEnum } from "@/lib/autogpt-server-api";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../ui/select";
|
||||
import { Filter } from "lucide-react";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { useLibraryPageContext } from "./providers/LibraryAgentProvider";
|
||||
|
||||
const LibraryAgentFilter = ({}: {}) => {
|
||||
const api = useBackendAPI();
|
||||
const { setAgentLoading, setAgents, setLibraryFilter, searchTerm } =
|
||||
useLibraryPageContext();
|
||||
const handleSortChange = async (value: LibraryAgentFilterEnum) => {
|
||||
setLibraryFilter(value);
|
||||
setAgentLoading(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
let response = await api.librarySearchAgent(searchTerm, value, undefined);
|
||||
setAgents(response.agents);
|
||||
setAgentLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<span className="hidden sm:inline">sort by</span>
|
||||
<Select onValueChange={handleSortChange}>
|
||||
<SelectTrigger className="ml-1 w-fit space-x-1 border-none pl-2 shadow-md">
|
||||
<Filter className="h-4 w-4 sm:hidden" />
|
||||
<SelectValue
|
||||
placeholder="Last Modified"
|
||||
className={
|
||||
"font-sans text-[14px] font-[500] leading-[24px] text-neutral-600"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup
|
||||
className={
|
||||
"font-sans text-[14px] font-[500] leading-[24px] text-neutral-600"
|
||||
}
|
||||
>
|
||||
<SelectItem value={LibraryAgentFilterEnum.CREATED_AT}>
|
||||
Creation Date
|
||||
</SelectItem>
|
||||
<SelectItem value={LibraryAgentFilterEnum.UPDATED_AT}>
|
||||
Last Modified
|
||||
</SelectItem>
|
||||
<SelectItem value={LibraryAgentFilterEnum.IS_FAVOURITE}>
|
||||
Favorites
|
||||
</SelectItem>
|
||||
<SelectItem value={LibraryAgentFilterEnum.IS_CREATED_BY_USER}>
|
||||
Created By Me
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LibraryAgentFilter;
|
||||
@@ -0,0 +1,183 @@
|
||||
import { Button } from "../ui/button";
|
||||
import Image from "next/image";
|
||||
import { Separator } from "../ui/separator";
|
||||
import {
|
||||
CirclePlayIcon,
|
||||
ClipboardCopy,
|
||||
ImageIcon,
|
||||
Play,
|
||||
PlayCircle,
|
||||
Share2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
|
||||
export interface NotificationCardData {
|
||||
type: "text" | "image" | "video" | "audio";
|
||||
title: string;
|
||||
id: string;
|
||||
content?: string;
|
||||
mediaUrl?: string;
|
||||
}
|
||||
|
||||
interface NotificationCardProps extends NotificationCardData {
|
||||
setNotifications: Dispatch<SetStateAction<NotificationCardData[] | null>>;
|
||||
}
|
||||
|
||||
const NotificationCard = ({
|
||||
type,
|
||||
title,
|
||||
id,
|
||||
content,
|
||||
mediaUrl,
|
||||
setNotifications,
|
||||
}: NotificationCardProps) => {
|
||||
const barHeights = Array.from({ length: 60 }, () =>
|
||||
Math.floor(Math.random() * (34 - 20 + 1) + 20),
|
||||
);
|
||||
|
||||
const handleClose = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
setNotifications((prev) => {
|
||||
if (!prev) return null;
|
||||
return prev.filter((notification) => notification.id !== id);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-[430px] space-y-[22px] rounded-[14px] border border-neutral-100 bg-neutral-50 p-[16px] pt-[12px]">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* count */}
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<p className="font-sans text-[12px] font-medium text-neutral-500">
|
||||
1/4
|
||||
</p>
|
||||
<p className="h-[26px] rounded-[45px] bg-green-100 px-[9px] py-[3px] font-sans text-[12px] font-medium text-green-800">
|
||||
Success
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* cross icon */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="p-0 hover:bg-transparent"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<X
|
||||
className="h-6 w-6 text-[#020617] hover:scale-105"
|
||||
strokeWidth={1.25}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-[6px] p-0">
|
||||
<p className="font-sans text-[14px] font-medium leading-[20px] text-neutral-500">
|
||||
New Output Ready!
|
||||
</p>
|
||||
<h2 className="font-poppin text-[20px] font-medium leading-7 text-neutral-800">
|
||||
{title}
|
||||
</h2>
|
||||
{type === "text" && <Separator />}
|
||||
</div>
|
||||
|
||||
<div className="p-0">
|
||||
{type === "text" && (
|
||||
// Maybe in future we give markdown support
|
||||
<div className="mt-[-8px] line-clamp-6 font-sans text-sm font-[400px] text-neutral-600">
|
||||
{content}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type === "image" &&
|
||||
(mediaUrl ? (
|
||||
<div className="relative h-[200px] w-full">
|
||||
<Image
|
||||
src={mediaUrl}
|
||||
alt={title}
|
||||
fill
|
||||
className="rounded-lg object-cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[244px] w-full items-center justify-center rounded-lg bg-[#D9D9D9]">
|
||||
<ImageIcon
|
||||
className="h-[138px] w-[138px] text-neutral-400"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{type === "video" && (
|
||||
<div className="space-y-4">
|
||||
{mediaUrl ? (
|
||||
<video src={mediaUrl} controls className="w-full rounded-lg" />
|
||||
) : (
|
||||
<div className="flex h-[219px] w-[398px] items-center justify-center rounded-lg bg-[#D9D9D9]">
|
||||
<PlayCircle
|
||||
className="h-16 w-16 text-neutral-500"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type === "audio" && (
|
||||
<div className="flex gap-2">
|
||||
<CirclePlayIcon
|
||||
className="h-10 w-10 rounded-full bg-neutral-800 text-white"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
{/* <audio src={mediaUrl} controls className="w-full" /> */}
|
||||
{barHeights.map((h, i) => {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={`rounded-[8px] bg-neutral-500`}
|
||||
style={{
|
||||
height: `${h}px`,
|
||||
width: "3px",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-2 p-0">
|
||||
<div className="space-x-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
navigator.share({
|
||||
title,
|
||||
text: content,
|
||||
url: mediaUrl,
|
||||
});
|
||||
}}
|
||||
className="h-10 w-10 rounded-full border-neutral-800 p-0"
|
||||
>
|
||||
<Share2 className="h-5 w-5" strokeWidth={1} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
navigator.clipboard.writeText(content || mediaUrl || "")
|
||||
}
|
||||
className="h-10 w-10 rounded-full border-neutral-800 p-0"
|
||||
>
|
||||
<ClipboardCopy className="h-5 w-5" strokeWidth={1} />
|
||||
</Button>
|
||||
</div>
|
||||
<Button className="h-[40px] rounded-[52px] bg-neutral-800 px-4 py-2">
|
||||
See run
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationCard;
|
||||
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "./Button";
|
||||
import { BellIcon, X } from "lucide-react";
|
||||
import { motion, useAnimationControls, useScroll } from "framer-motion";
|
||||
import { useState, useEffect } from "react";
|
||||
import LibraryNotificationCard, {
|
||||
NotificationCardData,
|
||||
} from "./LibraryNotificationCard";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const LibraryNotificationDropdown = () => {
|
||||
const controls = useAnimationControls();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [notifications, setNotifications] = useState<
|
||||
NotificationCardData[] | null
|
||||
>(null);
|
||||
const { scrollY } = useScroll();
|
||||
const [scrollPosition, setScrollPosition] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = scrollY.onChange((currentY) => {
|
||||
setScrollPosition(currentY);
|
||||
});
|
||||
return () => unsubscribe();
|
||||
}, [scrollY]);
|
||||
|
||||
const initialNotificationData = [
|
||||
{
|
||||
type: "audio" as "audio",
|
||||
title: "Audio Processing Complete",
|
||||
id: "4",
|
||||
},
|
||||
{
|
||||
type: "text" as "text",
|
||||
title: "LinkedIn Post Generator: YouTube to Professional Content",
|
||||
id: "1",
|
||||
content:
|
||||
"As artificial intelligence (AI) continues to evolve, it's increasingly clear that AI isn't just a trend—it's reshaping the way we work, innovate, and solve complex problems. However, for many professionals, the question remains: How can I leverage AI to drive meaningful results in my own field? In this article, we'll explore how AI can empower businesses and individuals alike to be more efficient, make better decisions, and unlock new opportunities. Whether you're in tech, finance, healthcare, or any other industry, understanding the potential of AI can set you apart.",
|
||||
},
|
||||
{
|
||||
type: "image" as "image",
|
||||
title: "New Image Upload",
|
||||
id: "2",
|
||||
},
|
||||
{
|
||||
type: "video" as "video",
|
||||
title: "Video Processing Complete",
|
||||
id: "3",
|
||||
},
|
||||
] as NotificationCardData[];
|
||||
|
||||
useEffect(() => {
|
||||
if (initialNotificationData) {
|
||||
setNotifications(initialNotificationData);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleHoverStart = () => {
|
||||
controls.start({
|
||||
rotate: [0, -10, 10, -10, 10, 0],
|
||||
transition: { duration: 0.5 },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger className="sm:flex-1" asChild>
|
||||
<Button
|
||||
variant={open ? "library_primary" : "library_outline"}
|
||||
size="library"
|
||||
onMouseEnter={handleHoverStart}
|
||||
onMouseLeave={handleHoverStart}
|
||||
className={cn(
|
||||
"z-50 max-w-[161px] transition-all duration-200 ease-in-out",
|
||||
scrollY.get() > 30 ? "w-fit max-w-fit" : "w-fit sm:w-[161px]",
|
||||
)}
|
||||
>
|
||||
<motion.div animate={controls}>
|
||||
<BellIcon
|
||||
className={cn(
|
||||
"h-5 w-5 transition-all duration-200 ease-in-out",
|
||||
scrollY.get() <= 30 && "sm:mr-2",
|
||||
)}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</motion.div>
|
||||
{scrollY.get() <= 30 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 1 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="hidden items-center transition-opacity duration-300 sm:inline-flex"
|
||||
>
|
||||
Your updates
|
||||
<span className="ml-2 text-[14px]">
|
||||
{notifications?.length || 0}
|
||||
</span>
|
||||
</motion.div>
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
sideOffset={22}
|
||||
className="scroll-none relative left-[16px] h-[80vh] w-fit overflow-y-auto rounded-[26px] bg-[#C5C5CA] p-5"
|
||||
>
|
||||
<DropdownMenuLabel className="z-10 mb-4 font-sans text-[18px] text-white">
|
||||
Agent run updates
|
||||
</DropdownMenuLabel>
|
||||
<button
|
||||
className="absolute right-[10px] top-[20px] h-fit w-fit"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<X className="h-6 w-6 text-white hover:text-white/60" />
|
||||
</button>
|
||||
<div className="space-y-[12px]">
|
||||
{notifications && notifications.length ? (
|
||||
notifications.map((notification) => (
|
||||
<DropdownMenuItem key={notification.id} className="p-0">
|
||||
<LibraryNotificationCard
|
||||
{...notification}
|
||||
setNotifications={setNotifications}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
) : (
|
||||
<div className="w-[464px] py-4 text-center text-white">
|
||||
No notifications present
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
import { Search, X } from "lucide-react";
|
||||
import { Input } from "../ui/input";
|
||||
import { useRef, useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import debounce from "lodash/debounce";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { useLibraryPageContext } from "./providers/LibraryAgentProvider";
|
||||
|
||||
export const LibrarySearchBar = () => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const api = useBackendAPI();
|
||||
const { setAgentLoading, setAgents, libraryFilter, setSearchTerm } =
|
||||
useLibraryPageContext();
|
||||
|
||||
const debouncedSearch = debounce(async (value: string) => {
|
||||
try {
|
||||
setAgentLoading(true);
|
||||
setSearchTerm(value);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
const response = await api.librarySearchAgent(value, libraryFilter);
|
||||
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]"
|
||||
>
|
||||
<div className="w-[30px] overflow-hidden">
|
||||
<AnimatePresence mode="wait">
|
||||
{!isFocused ? (
|
||||
<motion.div
|
||||
key="search"
|
||||
initial={{ x: -50 }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: -50 }}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
>
|
||||
<Search
|
||||
className="h-[29px] w-[29px] text-neutral-900"
|
||||
strokeWidth={1.25}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="close"
|
||||
initial={{ x: 50 }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: 50 }}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
>
|
||||
<X
|
||||
className="h-[29px] w-[29px] cursor-pointer text-neutral-900"
|
||||
strokeWidth={1.25}
|
||||
onClick={(e) => {
|
||||
if (inputRef.current) {
|
||||
debouncedSearch("");
|
||||
inputRef.current.value = "";
|
||||
inputRef.current.blur();
|
||||
e.preventDefault();
|
||||
}
|
||||
setIsFocused(false);
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
ref={inputRef}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => !inputRef.current?.value && setIsFocused(false)}
|
||||
onChange={handleSearchInput}
|
||||
className="border-none font-sans text-[16px] font-normal leading-7 shadow-none focus:shadow-none"
|
||||
type="text"
|
||||
placeholder="Search agents"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,323 @@
|
||||
"use client";
|
||||
import { Upload, X } from "lucide-react";
|
||||
import { Button } from "./Button";
|
||||
import { useEffect, useState } from "react";
|
||||
import { motion, useAnimation } from "framer-motion";
|
||||
import { cn, removeCredentials } from "@/lib/utils";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../ui/dialog";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "../ui/input";
|
||||
import { FileUploader } from "react-drag-drop-files";
|
||||
import { Graph, GraphCreatable } from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
const fileTypes = ["JSON"];
|
||||
|
||||
const fileSchema = z.custom<File>((val) => val instanceof File, {
|
||||
message: "Must be a File object",
|
||||
});
|
||||
|
||||
const formSchema = z.object({
|
||||
agentFile: fileSchema,
|
||||
agentName: z.string().min(1, "Agent name is required"),
|
||||
agentDescription: z.string(),
|
||||
});
|
||||
|
||||
function updateBlockIDs(graph: Graph) {
|
||||
const updatedBlockIDMap: Record<string, string> = {
|
||||
"a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6":
|
||||
"436c3984-57fd-4b85-8e9a-459b356883bd",
|
||||
"b2g2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6":
|
||||
"0e50422c-6dee-4145-83d6-3a5a392f65de",
|
||||
"c3d4e5f6-7g8h-9i0j-1k2l-m3n4o5p6q7r8":
|
||||
"a0a69be1-4528-491c-a85a-a4ab6873e3f0",
|
||||
"c3d4e5f6-g7h8-i9j0-k1l2-m3n4o5p6q7r8":
|
||||
"32a87eab-381e-4dd4-bdb8-4c47151be35a",
|
||||
"b2c3d4e5-6f7g-8h9i-0j1k-l2m3n4o5p6q7":
|
||||
"87840993-2053-44b7-8da4-187ad4ee518c",
|
||||
"h1i2j3k4-5l6m-7n8o-9p0q-r1s2t3u4v5w6":
|
||||
"d0822ab5-9f8a-44a3-8971-531dd0178b6b",
|
||||
"d3f4g5h6-1i2j-3k4l-5m6n-7o8p9q0r1s2t":
|
||||
"df06086a-d5ac-4abb-9996-2ad0acb2eff7",
|
||||
"h5e7f8g9-1b2c-3d4e-5f6g-7h8i9j0k1l2m":
|
||||
"f5b0f5d0-1862-4d61-94be-3ad0fa772760",
|
||||
"a1234567-89ab-cdef-0123-456789abcdef":
|
||||
"4335878a-394e-4e67-adf2-919877ff49ae",
|
||||
"f8e7d6c5-b4a3-2c1d-0e9f-8g7h6i5j4k3l":
|
||||
"f66a3543-28d3-4ab5-8945-9b336371e2ce",
|
||||
"b29c1b50-5d0e-4d9f-8f9d-1b0e6fcbf0h2":
|
||||
"716a67b3-6760-42e7-86dc-18645c6e00fc",
|
||||
"31d1064e-7446-4693-o7d4-65e5ca9110d1":
|
||||
"cc10ff7b-7753-4ff2-9af6-9399b1a7eddc",
|
||||
"c6731acb-4105-4zp1-bc9b-03d0036h370g":
|
||||
"5ebe6768-8e5d-41e3-9134-1c7bd89a8d52",
|
||||
};
|
||||
graph.nodes
|
||||
.filter((node) => node.block_id in updatedBlockIDMap)
|
||||
.forEach((node) => {
|
||||
node.block_id = updatedBlockIDMap[node.block_id];
|
||||
});
|
||||
return graph;
|
||||
}
|
||||
|
||||
export const LibraryUploadAgent = () => {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [isDroped, setisDroped] = useState(false);
|
||||
const controls = useAnimation();
|
||||
const api = useBackendAPI();
|
||||
const [agentObject, setAgentObject] = useState<GraphCreatable | null>(null);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
agentName: "",
|
||||
agentDescription: "",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (window.scrollY > 30) {
|
||||
setScrolled(true);
|
||||
} else {
|
||||
setScrolled(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
const onSubmit = (values: z.infer<typeof formSchema>) => {
|
||||
if (!agentObject) {
|
||||
form.setError("root", { message: "No Agent object to save" });
|
||||
return;
|
||||
}
|
||||
const payload: GraphCreatable = {
|
||||
...agentObject,
|
||||
name: values.agentName,
|
||||
description: values.agentDescription,
|
||||
is_active: true,
|
||||
};
|
||||
|
||||
api
|
||||
.createGraph(payload)
|
||||
.then((response) => {
|
||||
const qID = "flowID";
|
||||
window.location.href = `/build?${qID}=${response.id}`;
|
||||
})
|
||||
.catch((error) => {
|
||||
form.setError("root", {
|
||||
message: `Could not create agent: ${error}`,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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;
|
||||
removeCredentials(agent);
|
||||
updateBlockIDs(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 (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="library_primary"
|
||||
size="library"
|
||||
className={cn(
|
||||
"max-w-[177px] transition-all duration-200 ease-in-out",
|
||||
scrolled ? "w-fit max-w-fit" : "w-fit sm:w-[177px]",
|
||||
)}
|
||||
>
|
||||
<motion.div animate={controls}>
|
||||
<Upload
|
||||
className={cn(
|
||||
"h-5 w-5 transition-all duration-200 ease-in-out",
|
||||
!scrolled && "sm:mr-2",
|
||||
)}
|
||||
/>
|
||||
</motion.div>
|
||||
{!scrolled && (
|
||||
<motion.div
|
||||
initial={{ opacity: 1 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="hidden items-center transition-opacity duration-300 sm:inline-flex"
|
||||
>
|
||||
Upload an agent
|
||||
</motion.div>
|
||||
)}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="mb-8 text-center">Upload Agent</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="agentName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Agent name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} className="w-full rounded-[10px]" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="agentDescription"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea {...field} className="w-full rounded-[10px]" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="agentFile"
|
||||
render={({ field }) => (
|
||||
<FormItem className="rounded-xl border-2 border-dashed border-neutral-300 hover:border-neutral-600">
|
||||
<FormControl>
|
||||
{field.value ? (
|
||||
<div className="flex rounded-[10px] border p-2 font-sans text-sm font-medium text-[#525252] outline-none">
|
||||
<span className="line-clamp-1">{field.value.name}</span>
|
||||
<Button
|
||||
onClick={() =>
|
||||
form.setValue("agentFile", undefined as any)
|
||||
}
|
||||
className="absolute left-[-10px] top-[-16px] mt-2 h-fit border-none bg-red-200 p-1"
|
||||
size="library"
|
||||
>
|
||||
<X
|
||||
className="m-0 h-[12px] w-[12px] text-red-600"
|
||||
strokeWidth={3}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<FileUploader
|
||||
handleChange={handleChange}
|
||||
name="file"
|
||||
types={fileTypes}
|
||||
label={"Upload your agent here..!!"}
|
||||
uploadedLabel={"Uploading Successful"}
|
||||
required={true}
|
||||
hoverTitle={"Drop your agent here...!!"}
|
||||
maxSize={10}
|
||||
classes={"drop-style"}
|
||||
onDrop={() => {
|
||||
setisDroped(true);
|
||||
}}
|
||||
onSelect={() => setisDroped(true)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
minHeight: "150px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
outline: "none",
|
||||
fontFamily: "var(--font-geist-sans)",
|
||||
color: "#525252",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
borderWidth: "0px",
|
||||
}}
|
||||
>
|
||||
{isDroped ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-t-2 border-neutral-800"></div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span>Drop your agent here</span>
|
||||
<span>or</span>
|
||||
<span>Click to upload</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</FileUploader>
|
||||
)}
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="library_primary"
|
||||
size="library"
|
||||
className="mt-2 self-end"
|
||||
disabled={!agentObject}
|
||||
>
|
||||
Upload Agent
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
"use client";
|
||||
import { LibrarySearchBar } from "@/components/agptui/LibrarySearchBar";
|
||||
import { LibraryNotificationDropdown } from "../LibraryNotificationDropdown";
|
||||
import { LibraryUploadAgent } from "../LibraryUploadAgent";
|
||||
import { motion, useScroll, useTransform } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import LibraryAgentFilter from "../LibraryAgentFilter";
|
||||
import { useLibraryPageContext } from "../providers/LibraryAgentProvider";
|
||||
|
||||
interface LibraryActionHeaderProps {}
|
||||
|
||||
// Constants for header animation behavior
|
||||
const SCROLL_THRESHOLD = 30;
|
||||
const INITIAL_HEIGHT = 100;
|
||||
const COLLAPSED_HEIGHT = 50;
|
||||
const TRANSITION_DURATION = 0.3;
|
||||
|
||||
/**
|
||||
* LibraryActionHeader component - Renders a sticky header with search, notifications and filters
|
||||
* Animates and collapses based on scroll position
|
||||
*/
|
||||
const LibraryActionHeader: React.FC<LibraryActionHeaderProps> = ({}) => {
|
||||
const { scrollY } = useScroll();
|
||||
const [scrollPosition, setScrollPosition] = useState(0);
|
||||
const { agents } = useLibraryPageContext();
|
||||
|
||||
const height = useTransform(
|
||||
scrollY,
|
||||
[0, 100],
|
||||
[INITIAL_HEIGHT, COLLAPSED_HEIGHT],
|
||||
);
|
||||
|
||||
const handleScroll = useCallback((currentY: number) => {
|
||||
setScrollPosition(currentY);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = scrollY.on("change", handleScroll);
|
||||
return () => unsubscribe();
|
||||
}, [scrollY, handleScroll]);
|
||||
|
||||
// Calculate animation offsets based on scroll position
|
||||
const getScrollAnimation = (offsetX: number, offsetY: number) => ({
|
||||
x: scrollPosition > SCROLL_THRESHOLD ? offsetX : 0,
|
||||
y: scrollPosition > SCROLL_THRESHOLD ? offsetY : 0,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sticky top-16 z-[10] hidden items-start justify-between bg-neutral-50 pb-4 md:flex">
|
||||
<motion.div
|
||||
className={cn("relative flex-1 space-y-[32px]")}
|
||||
style={{ height }}
|
||||
transition={{ duration: TRANSITION_DURATION }}
|
||||
>
|
||||
<LibraryNotificationDropdown />
|
||||
|
||||
<motion.div
|
||||
className="flex items-center gap-[10px] p-2"
|
||||
animate={getScrollAnimation(60, -76)}
|
||||
>
|
||||
<span className="w-[96px] font-poppin text-[18px] font-semibold leading-[28px] text-neutral-800">
|
||||
My agents
|
||||
</span>
|
||||
<span className="w-[70px] font-sans text-[14px] font-normal leading-6">
|
||||
{agents.length} agents
|
||||
</span>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
<LibrarySearchBar />
|
||||
<motion.div
|
||||
className="flex flex-1 flex-col items-end space-y-[32px]"
|
||||
style={{ height }}
|
||||
transition={{ duration: TRANSITION_DURATION }}
|
||||
>
|
||||
<LibraryUploadAgent />
|
||||
<motion.div
|
||||
className="flex items-center gap-[10px] pl-2 pr-2 font-sans text-[14px] font-[500] leading-[24px] text-neutral-600"
|
||||
animate={getScrollAnimation(-60, -68)}
|
||||
>
|
||||
<LibraryAgentFilter />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Mobile and tablet */}
|
||||
<div className="flex flex-col gap-4 bg-neutral-50 p-4 pt-[52px] md:hidden">
|
||||
<div className="flex w-full justify-between">
|
||||
<LibraryNotificationDropdown />
|
||||
<LibraryUploadAgent />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center">
|
||||
<LibrarySearchBar />
|
||||
</div>
|
||||
|
||||
<div className="flex w-full justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-poppin text-[18px] font-semibold leading-[28px] text-neutral-800">
|
||||
My agents
|
||||
</span>
|
||||
<span className="font-sans text-[14px] font-normal leading-6">
|
||||
{agents.length} agents
|
||||
</span>
|
||||
</div>
|
||||
<LibraryAgentFilter />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LibraryActionHeader;
|
||||
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { LibraryAgentCard } from "../LibraryAgentCard";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { GraphMeta } from "@/lib/autogpt-server-api";
|
||||
import { useThreshold } from "@/hooks/useThreshold";
|
||||
import { useLibraryPageContext } from "../providers/LibraryAgentProvider";
|
||||
|
||||
interface LibraryAgentListContainerProps {}
|
||||
|
||||
export type AgentStatus =
|
||||
| "healthy"
|
||||
| "something wrong"
|
||||
| "waiting for trigger"
|
||||
| "Nothing running";
|
||||
|
||||
/**
|
||||
* LibraryAgentListContainer is a React component that displays a grid of library agents with infinite scroll functionality.
|
||||
*/
|
||||
|
||||
const LibraryAgentListContainer: React.FC<
|
||||
LibraryAgentListContainerProps
|
||||
> = ({}) => {
|
||||
const [nextToken, setNextToken] = useState<string | null>(null);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
|
||||
const api = useBackendAPI();
|
||||
const { agents, setAgents, setAgentLoading, agentLoading } =
|
||||
useLibraryPageContext();
|
||||
|
||||
const fetchAgents = useCallback(
|
||||
async (paginationToken?: string) => {
|
||||
try {
|
||||
const response = await api.listLibraryAgents(paginationToken);
|
||||
if (paginationToken) {
|
||||
setAgents((prevAgent) => [...prevAgent, ...response.agents]);
|
||||
} else {
|
||||
setAgents(response.agents);
|
||||
}
|
||||
setNextToken(response.next_token);
|
||||
} finally {
|
||||
setAgentLoading(false);
|
||||
setLoadingMore(false);
|
||||
}
|
||||
},
|
||||
[api, setAgents, setAgentLoading],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAgents();
|
||||
}, [fetchAgents]);
|
||||
|
||||
const handleInfiniteScroll = useCallback(
|
||||
(scrollY: number) => {
|
||||
if (!nextToken || loadingMore) return;
|
||||
|
||||
const { scrollHeight, clientHeight } = document.documentElement;
|
||||
const SCROLL_THRESHOLD = 20;
|
||||
const FETCH_DELAY = 1000;
|
||||
|
||||
if (scrollY + clientHeight >= scrollHeight - SCROLL_THRESHOLD) {
|
||||
setLoadingMore(true);
|
||||
setTimeout(() => fetchAgents(nextToken), FETCH_DELAY);
|
||||
}
|
||||
},
|
||||
[nextToken, loadingMore, fetchAgents],
|
||||
);
|
||||
|
||||
useThreshold(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="space-y-[10px] p-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-4 xl:grid-cols-4">
|
||||
{agents?.map((agent) => (
|
||||
<LibraryAgentCard
|
||||
key={agent.id}
|
||||
id={agent.id}
|
||||
name={agent.name}
|
||||
isCreatedByUser={agent.isCreatedByUser}
|
||||
input_schema={agent.input_schema}
|
||||
output_schema={agent.output_schema}
|
||||
is_active={agent.is_active}
|
||||
version={agent.version}
|
||||
description={agent.description}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{loadingMore && (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LibraryAgentListContainer;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { GraphMeta, LibraryAgentFilterEnum } from "@/lib/autogpt-server-api";
|
||||
import {
|
||||
createContext,
|
||||
useState,
|
||||
ReactNode,
|
||||
useContext,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
} from "react";
|
||||
|
||||
interface LibraryPageContextType {
|
||||
agents: GraphMeta[];
|
||||
setAgents: Dispatch<SetStateAction<GraphMeta[]>>;
|
||||
agentLoading: boolean;
|
||||
setAgentLoading: Dispatch<SetStateAction<boolean>>;
|
||||
searchTerm: string | undefined;
|
||||
setSearchTerm: Dispatch<SetStateAction<string | undefined>>;
|
||||
uploadedFile: File | null;
|
||||
setUploadedFile: Dispatch<SetStateAction<File | null>>;
|
||||
libraryFilter: LibraryAgentFilterEnum;
|
||||
setLibraryFilter: Dispatch<SetStateAction<LibraryAgentFilterEnum>>;
|
||||
}
|
||||
|
||||
export const LibraryPageContext = createContext<LibraryPageContextType>(
|
||||
{} as LibraryPageContextType,
|
||||
);
|
||||
|
||||
interface LibraryPageProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function LibraryPageProvider({ children }: LibraryPageProviderProps) {
|
||||
const [agents, setAgents] = useState<GraphMeta[]>([]);
|
||||
const [agentLoading, setAgentLoading] = useState<boolean>(true);
|
||||
const [searchTerm, setSearchTerm] = useState<string | undefined>("");
|
||||
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
|
||||
const [libraryFilter, setLibraryFilter] = useState<LibraryAgentFilterEnum>(
|
||||
LibraryAgentFilterEnum.UPDATED_AT,
|
||||
);
|
||||
|
||||
return (
|
||||
<LibraryPageContext.Provider
|
||||
value={{
|
||||
agents,
|
||||
setAgents,
|
||||
agentLoading,
|
||||
setAgentLoading,
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
uploadedFile,
|
||||
setUploadedFile,
|
||||
libraryFilter,
|
||||
setLibraryFilter,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LibraryPageContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLibraryPageContext(): LibraryPageContextType {
|
||||
const context = useContext(LibraryPageContext);
|
||||
if (!context) {
|
||||
throw new Error("Error in context of Library page");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -88,7 +88,7 @@ const DropdownMenuItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
|
||||
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
|
||||
37
autogpt_platform/frontend/src/hooks/useThreshold.ts
Normal file
37
autogpt_platform/frontend/src/hooks/useThreshold.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface ThresholdCallback<T> {
|
||||
(value: T): void;
|
||||
}
|
||||
|
||||
export const useThreshold = <T>(
|
||||
callback: ThresholdCallback<T>,
|
||||
threshold: number,
|
||||
): boolean => {
|
||||
const [prevValue, setPrevValue] = useState<T | null>(null);
|
||||
const [isThresholdMet, setIsThresholdMet] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const { scrollY } = window;
|
||||
|
||||
if (scrollY >= threshold) {
|
||||
setIsThresholdMet(true);
|
||||
} else {
|
||||
setIsThresholdMet(false);
|
||||
}
|
||||
|
||||
if (scrollY >= threshold && (!prevValue || prevValue !== scrollY)) {
|
||||
callback(scrollY as T);
|
||||
setPrevValue(scrollY as T);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
handleScroll();
|
||||
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, [callback, threshold, prevValue]);
|
||||
|
||||
return isThresholdMet;
|
||||
};
|
||||
@@ -445,11 +445,39 @@ export default class BackendAPI {
|
||||
/////////// V2 LIBRARY API //////////////
|
||||
/////////////////////////////////////////
|
||||
|
||||
async listLibraryAgents(): Promise<GraphMeta[]> {
|
||||
return this._get("/library/agents");
|
||||
async listLibraryAgents(
|
||||
paginationToken?: string,
|
||||
): Promise<{ agents: GraphMeta[]; next_token: string | null }> {
|
||||
return this._get(
|
||||
"/library/agents",
|
||||
paginationToken ? { pagination_token: paginationToken } : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
async librarySearchAgent(
|
||||
search?: string,
|
||||
filter?: LibraryAgentFilterEnum,
|
||||
token?: string,
|
||||
): Promise<{ agents: GraphMeta[]; next_token: string | null }> {
|
||||
const queryParams: Record<string, any> = {};
|
||||
|
||||
if (search != undefined) {
|
||||
queryParams.search_term = search;
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
queryParams.sort_by = filter;
|
||||
}
|
||||
|
||||
if (token) {
|
||||
queryParams.pagination_token = token;
|
||||
}
|
||||
|
||||
return this._get("/library/agents/search", queryParams);
|
||||
}
|
||||
|
||||
async addAgentToLibrary(storeListingVersionId: string): Promise<void> {
|
||||
console.log("Adding to the library");
|
||||
await this._request("POST", `/library/agents/${storeListingVersionId}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -233,6 +233,7 @@ export type GraphMeta = {
|
||||
version: number;
|
||||
is_active: boolean;
|
||||
name: string;
|
||||
isCreatedByUser?: boolean;
|
||||
description: string;
|
||||
input_schema: BlockIOObjectSubSchema;
|
||||
output_schema: BlockIOObjectSubSchema;
|
||||
|
||||
@@ -1041,6 +1041,23 @@
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@emotion/is-prop-valid@1.2.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz#d4175076679c6a26faa92b03bb786f9e52612337"
|
||||
integrity sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==
|
||||
dependencies:
|
||||
"@emotion/memoize" "^0.8.1"
|
||||
|
||||
"@emotion/memoize@^0.8.1":
|
||||
version "0.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17"
|
||||
integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==
|
||||
|
||||
"@emotion/unitless@0.8.1":
|
||||
version "0.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3"
|
||||
integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==
|
||||
|
||||
"@esbuild/aix-ppc64@0.24.0":
|
||||
version "0.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz#b57697945b50e99007b4c2521507dc613d4a648c"
|
||||
@@ -3699,6 +3716,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/junit-report-builder/-/junit-report-builder-3.0.2.tgz#17cc131d14ceff59dcf14e5847bd971b96f2cbe0"
|
||||
integrity sha512-R5M+SYhMbwBeQcNXYWNCZkl09vkVfAtcPIaCGdzIkkbeaTrVbGQ7HVgi4s+EmM/M1K4ZuWQH0jGcvMvNePfxYA==
|
||||
|
||||
"@types/lodash@^4.17.13":
|
||||
version "4.17.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.13.tgz#786e2d67cfd95e32862143abe7463a7f90c300eb"
|
||||
integrity sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==
|
||||
|
||||
"@types/mdast@^4.0.0":
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6"
|
||||
@@ -3827,6 +3849,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/statuses/-/statuses-2.0.5.tgz#f61ab46d5352fd73c863a1ea4e1cef3b0b51ae63"
|
||||
integrity sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==
|
||||
|
||||
"@types/stylis@4.2.5":
|
||||
version "4.2.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/stylis/-/stylis-4.2.5.tgz#1daa6456f40959d06157698a653a9ab0a70281df"
|
||||
integrity sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==
|
||||
|
||||
"@types/tedious@^4.0.14":
|
||||
version "4.0.14"
|
||||
resolved "https://registry.yarnpkg.com/@types/tedious/-/tedious-4.0.14.tgz#868118e7a67808258c05158e9cad89ca58a2aec1"
|
||||
@@ -4898,6 +4925,11 @@ camelcase@^6.2.0:
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
|
||||
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
|
||||
|
||||
camelize@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3"
|
||||
integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==
|
||||
|
||||
caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001669:
|
||||
version "1.0.30001688"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz#f9d3ede749f083ce0db4c13db9d828adaf2e8d0a"
|
||||
@@ -5358,6 +5390,11 @@ crypto-browserify@^3.12.0:
|
||||
randombytes "^2.1.0"
|
||||
randomfill "^1.0.4"
|
||||
|
||||
css-color-keywords@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05"
|
||||
integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==
|
||||
|
||||
css-loader@^6.7.1, css-loader@^6.7.3:
|
||||
version "6.11.0"
|
||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.11.0.tgz#33bae3bf6363d0a7c2cf9031c96c744ff54d85ba"
|
||||
@@ -5383,6 +5420,15 @@ css-select@^4.1.3:
|
||||
domutils "^2.8.0"
|
||||
nth-check "^2.0.1"
|
||||
|
||||
css-to-react-native@3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32"
|
||||
integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==
|
||||
dependencies:
|
||||
camelize "^1.0.0"
|
||||
css-color-keywords "^1.0.0"
|
||||
postcss-value-parser "^4.0.2"
|
||||
|
||||
css-what@^6.0.1:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
|
||||
@@ -5398,7 +5444,7 @@ cssesc@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
|
||||
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
|
||||
|
||||
csstype@^3.0.2:
|
||||
csstype@3.1.3, csstype@^3.0.2:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
||||
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
||||
@@ -8415,10 +8461,10 @@ lru-cache@^5.1.1:
|
||||
dependencies:
|
||||
yallist "^3.0.2"
|
||||
|
||||
lucide-react@^0.474.0:
|
||||
version "0.474.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.474.0.tgz#9fcaa96250fa2de0b3e2803d4ad744eaea572247"
|
||||
integrity sha512-CmghgHkh0OJNmxGKWc0qfPJCYHASPMVSyGY8fj3xgk4v84ItqDg64JNKFZn5hC6E0vHi6gxnbCgwhyVB09wQtA==
|
||||
lucide-react@^0.468.0:
|
||||
version "0.468.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.468.0.tgz#830c1bfd905575ddd23b832baa420c87db166910"
|
||||
integrity sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==
|
||||
|
||||
lz-string@^1.5.0:
|
||||
version "1.5.0"
|
||||
@@ -8968,7 +9014,7 @@ mz@^2.7.0:
|
||||
object-assign "^4.0.1"
|
||||
thenify-all "^1.0.0"
|
||||
|
||||
nanoid@^3.3.6, nanoid@^3.3.8:
|
||||
nanoid@^3.3.6, nanoid@^3.3.7, nanoid@^3.3.8:
|
||||
version "3.3.8"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
|
||||
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
|
||||
@@ -9667,7 +9713,7 @@ postcss-selector-parser@^7.0.0:
|
||||
cssesc "^3.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
|
||||
postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
@@ -9681,6 +9727,15 @@ postcss@8.4.31:
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
postcss@8.4.38:
|
||||
version "8.4.38"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
|
||||
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
|
||||
dependencies:
|
||||
nanoid "^3.3.7"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.2.0"
|
||||
|
||||
postcss@^8, postcss@^8.2.14, postcss@^8.4.33, postcss@^8.4.38, postcss@^8.4.47:
|
||||
version "8.5.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.1.tgz#e2272a1f8a807fafa413218245630b5db10a3214"
|
||||
@@ -9957,6 +10012,14 @@ react-docgen@^7.0.0:
|
||||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.2"
|
||||
|
||||
react-drag-drop-files@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/react-drag-drop-files/-/react-drag-drop-files-2.4.0.tgz#d4c4f14cf3e76bb7fb2734aed2174e7120e56733"
|
||||
integrity sha512-MGPV3HVVnwXEXq3gQfLtSU3jz5j5jrabvGedokpiSEMoONrDHgYl/NpIOlfsqGQ4zBv1bzzv7qbKURZNOX32PA==
|
||||
dependencies:
|
||||
prop-types "^15.7.2"
|
||||
styled-components "^6.1.11"
|
||||
|
||||
react-hook-form@^7.54.2:
|
||||
version "7.54.2"
|
||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.54.2.tgz#8c26ed54c71628dff57ccd3c074b1dd377cfb211"
|
||||
@@ -10540,6 +10603,11 @@ sha.js@^2.4.0, sha.js@^2.4.8:
|
||||
inherits "^2.0.1"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
shallowequal@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
|
||||
integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
|
||||
|
||||
sharp@^0.33.3:
|
||||
version "0.33.5"
|
||||
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.5.tgz#13e0e4130cc309d6a9497596715240b2ec0c594e"
|
||||
@@ -10672,7 +10740,7 @@ slash@^5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/slash/-/slash-5.1.0.tgz#be3adddcdf09ac38eebe8dcdc7b1a57a75b095ce"
|
||||
integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==
|
||||
|
||||
source-map-js@^1.0.2, source-map-js@^1.2.1:
|
||||
source-map-js@^1.0.2, source-map-js@^1.2.0, source-map-js@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
@@ -10998,6 +11066,21 @@ style-to-object@^1.0.0:
|
||||
dependencies:
|
||||
inline-style-parser "0.2.4"
|
||||
|
||||
styled-components@^6.1.11:
|
||||
version "6.1.13"
|
||||
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-6.1.13.tgz#2d777750b773b31469bd79df754a32479e9f475e"
|
||||
integrity sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw==
|
||||
dependencies:
|
||||
"@emotion/is-prop-valid" "1.2.2"
|
||||
"@emotion/unitless" "0.8.1"
|
||||
"@types/stylis" "4.2.5"
|
||||
css-to-react-native "3.2.0"
|
||||
csstype "3.1.3"
|
||||
postcss "8.4.38"
|
||||
shallowequal "1.1.0"
|
||||
stylis "4.3.2"
|
||||
tslib "2.6.2"
|
||||
|
||||
styled-jsx@5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f"
|
||||
@@ -11012,6 +11095,11 @@ styled-jsx@^5.1.6:
|
||||
dependencies:
|
||||
client-only "0.0.1"
|
||||
|
||||
stylis@4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.2.tgz#8f76b70777dd53eb669c6f58c997bf0a9972e444"
|
||||
integrity sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==
|
||||
|
||||
sucrase@^3.35.0:
|
||||
version "3.35.0"
|
||||
resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263"
|
||||
@@ -11261,6 +11349,11 @@ tsconfig-paths@^4.0.0, tsconfig-paths@^4.1.2, tsconfig-paths@^4.2.0:
|
||||
minimist "^1.2.6"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
tslib@2.6.2:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||
|
||||
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||
|
||||
Reference in New Issue
Block a user