mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-12 00:28:31 -05:00
Compare commits
295 Commits
feat/execu
...
Search-res
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05afc1e1bb | ||
|
|
a597507680 | ||
|
|
14c223435b | ||
|
|
f6ad194306 | ||
|
|
e5c21ceda9 | ||
|
|
1a4a3e72ec | ||
|
|
584b121a16 | ||
|
|
d67c7a3704 | ||
|
|
dec9dfad9d | ||
|
|
4af2297ca4 | ||
|
|
72e9dd6b37 | ||
|
|
987fe6d3b8 | ||
|
|
3c024be10c | ||
|
|
e508d90fa7 | ||
|
|
91fb4f5c56 | ||
|
|
32c15434d2 | ||
|
|
6fcceeabdb | ||
|
|
b1468f779c | ||
|
|
ac03211c59 | ||
|
|
c35453f41e | ||
|
|
07d732ece7 | ||
|
|
54baa84c28 | ||
|
|
0fb33f6cb7 | ||
|
|
afd809f68a | ||
|
|
a8ae006967 | ||
|
|
b7603f6053 | ||
|
|
c91099a5ef | ||
|
|
c7ae4cfbda | ||
|
|
b5e1c8075e | ||
|
|
d14cecb954 | ||
|
|
9bc72305c5 | ||
|
|
980c90c07c | ||
|
|
006e843461 | ||
|
|
452fe314f6 | ||
|
|
b86b0c758e | ||
|
|
f1053ff8b4 | ||
|
|
7a1176073d | ||
|
|
3145f3d59d | ||
|
|
89e8fe3854 | ||
|
|
6c34f8bd96 | ||
|
|
6ccf62c3a6 | ||
|
|
0217b75d65 | ||
|
|
6a712fd6bb | ||
|
|
f4244e5038 | ||
|
|
9c0e8750b0 | ||
|
|
14ecaf861e | ||
|
|
6d6ed348fa | ||
|
|
1d81c61b77 | ||
|
|
3da5fc2cac | ||
|
|
8d8664a3ce | ||
|
|
d201b653c8 | ||
|
|
f77172ec82 | ||
|
|
ec072ad52a | ||
|
|
9b456612aa | ||
|
|
3d0ec9c52a | ||
|
|
50d08654f8 | ||
|
|
fa871073ca | ||
|
|
ab4f4549d6 | ||
|
|
4492995d6b | ||
|
|
6a736cc60f | ||
|
|
60417545f0 | ||
|
|
4f20f868c1 | ||
|
|
2ed8347430 | ||
|
|
12eb3b2937 | ||
|
|
d5580f8a94 | ||
|
|
5b0ecfd26b | ||
|
|
3b24b0ac0d | ||
|
|
715d425579 | ||
|
|
0eda63fa15 | ||
|
|
4e886bd6e9 | ||
|
|
f3168ea187 | ||
|
|
7fdfffdfcc | ||
|
|
8f4d552909 | ||
|
|
303a55145d | ||
|
|
f54cfee4a7 | ||
|
|
b3bd0f5d54 | ||
|
|
91aa371220 | ||
|
|
1ab19dcc56 | ||
|
|
30a047eac3 | ||
|
|
f2387147c7 | ||
|
|
0f77f931ab | ||
|
|
fdb82eda38 | ||
|
|
53eee63161 | ||
|
|
48d27c91d4 | ||
|
|
39c9e4a76c | ||
|
|
b3fd8bbfb9 | ||
|
|
a4b186cf81 | ||
|
|
a971d59974 | ||
|
|
9db8832d6b | ||
|
|
acb35c3926 | ||
|
|
cd9c4218b0 | ||
|
|
3ccf0138b1 | ||
|
|
19ff8f324d | ||
|
|
f445918abf | ||
|
|
5a4083d542 | ||
|
|
1e66137c7e | ||
|
|
bb13157d18 | ||
|
|
37e6b6f385 | ||
|
|
fabf742601 | ||
|
|
100b667afc | ||
|
|
da10f1a2df | ||
|
|
65ada3fb72 | ||
|
|
bc277acf57 | ||
|
|
52d19d084c | ||
|
|
86a10858bd | ||
|
|
f297e42be4 | ||
|
|
2bbac5714f | ||
|
|
e063f4bcda | ||
|
|
6c64c5b98f | ||
|
|
5530db63de | ||
|
|
4278ae61b0 | ||
|
|
0c78edb592 | ||
|
|
93d3bd3773 | ||
|
|
c1c3fd4982 | ||
|
|
d4b69d864f | ||
|
|
db3284830a | ||
|
|
d16cf6cfeb | ||
|
|
3f3919a843 | ||
|
|
244171d748 | ||
|
|
faa683b6e4 | ||
|
|
9255759e1e | ||
|
|
abbed4051d | ||
|
|
8c5380d4f9 | ||
|
|
cacc6e1f86 | ||
|
|
9616baf695 | ||
|
|
518f196e6b | ||
|
|
a9693b582f | ||
|
|
64fcba3f3a | ||
|
|
a53f3f0e0a | ||
|
|
84cdf189f4 | ||
|
|
610a5b9943 | ||
|
|
18b5f2047c | ||
|
|
17db193faa | ||
|
|
78476630cd | ||
|
|
7a41f36b13 | ||
|
|
c315b8e700 | ||
|
|
f6c1bdccac | ||
|
|
afe5c12afb | ||
|
|
65344b9783 | ||
|
|
9f71fb940d | ||
|
|
28a327c57a | ||
|
|
6aba1bce62 | ||
|
|
5db220c568 | ||
|
|
8e63a4a8d7 | ||
|
|
175f17b131 | ||
|
|
9aec1f51ed | ||
|
|
760e2ff592 | ||
|
|
d17ea2d62a | ||
|
|
6351ba7f5d | ||
|
|
a1ba3b1ac3 | ||
|
|
d78b4d9ab4 | ||
|
|
1292c85d2a | ||
|
|
c44fd7332c | ||
|
|
45a2826df8 | ||
|
|
abb8134761 | ||
|
|
c879599871 | ||
|
|
a71b2a1de6 | ||
|
|
38b20e6158 | ||
|
|
a8a0da1e3c | ||
|
|
d3e7aab796 | ||
|
|
f539c24571 | ||
|
|
2068073e8c | ||
|
|
04d36194c9 | ||
|
|
e8eda51b27 | ||
|
|
aa5d304a2e | ||
|
|
9dab7c9132 | ||
|
|
4562606c54 | ||
|
|
fa933ada85 | ||
|
|
bc1d11bf42 | ||
|
|
5ac2e7044e | ||
|
|
7f741468dd | ||
|
|
715e4c7d73 | ||
|
|
ef473bbc8d | ||
|
|
43ac5e0343 | ||
|
|
da15408a35 | ||
|
|
bd00338690 | ||
|
|
c63ccb5bd9 | ||
|
|
0e9906ea65 | ||
|
|
ff0e786202 | ||
|
|
deee943c3a | ||
|
|
40f38fcb46 | ||
|
|
35ec676f35 | ||
|
|
c345a79962 | ||
|
|
b9c7d1a115 | ||
|
|
bf459a17ba | ||
|
|
15befae65f | ||
|
|
2340e9b3f5 | ||
|
|
7044782689 | ||
|
|
ef4776f697 | ||
|
|
946ba02969 | ||
|
|
9a6ff408b4 | ||
|
|
c07ea4f63d | ||
|
|
2af974b381 | ||
|
|
7e6bdf8b04 | ||
|
|
707c485212 | ||
|
|
5145aa7609 | ||
|
|
496990c096 | ||
|
|
52de22469f | ||
|
|
c27f163623 | ||
|
|
29a61abfe3 | ||
|
|
1580fb5fa7 | ||
|
|
b9763aa28a | ||
|
|
f9eefae1ad | ||
|
|
d16c1f259b | ||
|
|
f0650d1f76 | ||
|
|
d9710ce1af | ||
|
|
e5a4b9a5ac | ||
|
|
ebad48481e | ||
|
|
4503dab267 | ||
|
|
d73acd13cb | ||
|
|
a955794acd | ||
|
|
03d754cb50 | ||
|
|
8dc22e2a63 | ||
|
|
bacdc190e7 | ||
|
|
5943c75873 | ||
|
|
8db695932e | ||
|
|
a70d6a5193 | ||
|
|
4869a8ce22 | ||
|
|
d5d9ecd71c | ||
|
|
52119eadc2 | ||
|
|
334b4d5ef9 | ||
|
|
d7fc2dfb46 | ||
|
|
120469c8bf | ||
|
|
259d6f2b69 | ||
|
|
dce436fb30 | ||
|
|
44cb4e8e77 | ||
|
|
88767a84d1 | ||
|
|
8b03477d2d | ||
|
|
10a2b36dc9 | ||
|
|
b1ccdacd98 | ||
|
|
8811011286 | ||
|
|
b8c764ad70 | ||
|
|
4d69b2eb75 | ||
|
|
5adc6c0a46 | ||
|
|
915b08d8a7 | ||
|
|
2997b12367 | ||
|
|
82f0ee2240 | ||
|
|
26bef8b918 | ||
|
|
82f553ec0d | ||
|
|
4ea85b5eaf | ||
|
|
45efe5a947 | ||
|
|
c2b320dd6a | ||
|
|
6b2d264414 | ||
|
|
9a742cbe93 | ||
|
|
f96f2f101b | ||
|
|
6318a976b5 | ||
|
|
c2c39a0cd6 | ||
|
|
657e64d903 | ||
|
|
a2e681a09f | ||
|
|
1fc3b2aa0a | ||
|
|
5465296ba6 | ||
|
|
fbfb8838fd | ||
|
|
130261c75f | ||
|
|
c8c954d862 | ||
|
|
abfa707f69 | ||
|
|
a01f326f7e | ||
|
|
e377711c7e | ||
|
|
4c40b5f187 | ||
|
|
a8c5264e17 | ||
|
|
9e8f76b749 | ||
|
|
43f8ed55ea | ||
|
|
5d5f14c799 | ||
|
|
9db01a7836 | ||
|
|
1e4a96883f | ||
|
|
a34dc25b34 | ||
|
|
90d3954e8c | ||
|
|
aeb43b7d37 | ||
|
|
7dedcaddb6 | ||
|
|
02463a5cb2 | ||
|
|
4c18763e55 | ||
|
|
8a9a1b59a4 | ||
|
|
8d9b282376 | ||
|
|
ffdc457dea | ||
|
|
015ac85a83 | ||
|
|
ee01a602ff | ||
|
|
f7f4207902 | ||
|
|
37bae48bd9 | ||
|
|
2ef21f929f | ||
|
|
6b77d71b88 | ||
|
|
e2946e0cd1 | ||
|
|
887e7a4f0a | ||
|
|
c4a8ab8a19 | ||
|
|
a3dda5d5a2 | ||
|
|
948279a67d | ||
|
|
cf97e25b4a | ||
|
|
72e4eb2418 | ||
|
|
e241a4cc2a | ||
|
|
3accc65d44 | ||
|
|
fd6f23f4a6 | ||
|
|
5ad56c553f | ||
|
|
81471d8ffe | ||
|
|
e46bf6300e | ||
|
|
a0e87867b7 | ||
|
|
00d5d843a2 | ||
|
|
3ce7cf2713 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -173,4 +173,6 @@ LICENSE.rtf
|
||||
autogpt_platform/backend/settings.py
|
||||
/.auth
|
||||
/autogpt_platform/frontend/.auth
|
||||
.test-contents
|
||||
|
||||
*.ign.*
|
||||
.test-contents
|
||||
|
||||
@@ -35,3 +35,12 @@ def verify_user(payload: dict | None, admin_only: bool) -> User:
|
||||
raise fastapi.HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
return User.from_payload(payload)
|
||||
|
||||
|
||||
def get_user_id(payload: dict = fastapi.Depends(auth_middleware)) -> str:
|
||||
user_id = payload.get("sub")
|
||||
if not user_id:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=401, detail="User ID not found in token"
|
||||
)
|
||||
return user_id
|
||||
|
||||
@@ -6,18 +6,23 @@ ENV PYTHONUNBUFFERED 1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN echo 'Acquire::http::Pipeline-Depth 0;\nAcquire::http::No-Cache true;\nAcquire::BrokenProxy true;\n' > /etc/apt/apt.conf.d/99fixbadproxy
|
||||
|
||||
RUN apt-get update --allow-releaseinfo-change --fix-missing
|
||||
|
||||
# Install build dependencies
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y build-essential curl ffmpeg wget libcurl4-gnutls-dev libexpat1-dev libpq5 gettext libz-dev libssl-dev postgresql-client git \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV POETRY_VERSION=1.8.3 \
|
||||
POETRY_HOME="/opt/poetry" \
|
||||
POETRY_NO_INTERACTION=1 \
|
||||
POETRY_VIRTUALENVS_CREATE=false \
|
||||
PATH="$POETRY_HOME/bin:$PATH"
|
||||
RUN apt-get install -y build-essential
|
||||
RUN apt-get install -y libpq5
|
||||
RUN apt-get install -y libz-dev
|
||||
RUN apt-get install -y libssl-dev
|
||||
RUN apt-get install -y postgresql-client
|
||||
|
||||
ENV POETRY_VERSION=1.8.3
|
||||
ENV POETRY_HOME=/opt/poetry
|
||||
ENV POETRY_NO_INTERACTION=1
|
||||
ENV POETRY_VIRTUALENVS_CREATE=false
|
||||
ENV PATH=/opt/poetry/bin:$PATH
|
||||
|
||||
# Upgrade pip and setuptools to fix security vulnerabilities
|
||||
RUN pip3 install --upgrade pip setuptools
|
||||
|
||||
@@ -39,11 +44,11 @@ FROM python:3.11.10-slim-bookworm AS server_dependencies
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV POETRY_VERSION=1.8.3 \
|
||||
POETRY_HOME="/opt/poetry" \
|
||||
POETRY_NO_INTERACTION=1 \
|
||||
POETRY_VIRTUALENVS_CREATE=false \
|
||||
PATH="$POETRY_HOME/bin:$PATH"
|
||||
ENV POETRY_VERSION=1.8.3
|
||||
ENV POETRY_HOME=/opt/poetry
|
||||
ENV POETRY_NO_INTERACTION=1
|
||||
ENV POETRY_VIRTUALENVS_CREATE=false
|
||||
ENV PATH=/opt/poetry/bin:$PATH
|
||||
|
||||
|
||||
# Upgrade pip and setuptools to fix security vulnerabilities
|
||||
|
||||
@@ -143,7 +143,7 @@ class PineconeQueryBlock(Block):
|
||||
top_k=input_data.top_k,
|
||||
include_values=input_data.include_values,
|
||||
include_metadata=input_data.include_metadata,
|
||||
).to_dict()
|
||||
).to_dict() # type: ignore
|
||||
combined_text = ""
|
||||
if results["matches"]:
|
||||
texts = [
|
||||
|
||||
@@ -160,7 +160,7 @@ def SchemaField(
|
||||
exclude=exclude,
|
||||
json_schema_extra=json_extra,
|
||||
**kwargs,
|
||||
)
|
||||
) # type: ignore
|
||||
|
||||
|
||||
class _BaseCredentials(BaseModel):
|
||||
|
||||
@@ -16,6 +16,7 @@ import backend.data.db
|
||||
import backend.data.graph
|
||||
import backend.data.user
|
||||
import backend.server.routers.v1
|
||||
import backend.server.v2.store.routes
|
||||
import backend.util.service
|
||||
import backend.util.settings
|
||||
|
||||
@@ -84,7 +85,10 @@ def handle_internal_http_error(status_code: int = 500, log_error: bool = True):
|
||||
|
||||
app.add_exception_handler(ValueError, handle_internal_http_error(400))
|
||||
app.add_exception_handler(Exception, handle_internal_http_error(500))
|
||||
app.include_router(backend.server.routers.v1.v1_router, tags=["v1"])
|
||||
app.include_router(backend.server.routers.v1.v1_router, tags=["v1"], prefix="/api")
|
||||
app.include_router(
|
||||
backend.server.v2.store.routes.router, tags=["v2"], prefix="/api/store"
|
||||
)
|
||||
|
||||
|
||||
@app.get(path="/health", tags=["health"], dependencies=[])
|
||||
|
||||
@@ -69,8 +69,7 @@ integration_creds_manager = IntegrationCredentialsManager()
|
||||
_user_credit_model = get_user_credit_model()
|
||||
|
||||
# Define the API routes
|
||||
v1_router = APIRouter(prefix="/api")
|
||||
|
||||
v1_router = APIRouter()
|
||||
|
||||
v1_router.include_router(
|
||||
backend.server.integrations.router.router,
|
||||
@@ -132,7 +131,7 @@ def execute_graph_block(block_id: str, data: BlockInput) -> CompletedBlockOutput
|
||||
|
||||
@v1_router.get(path="/credits", dependencies=[Depends(auth_middleware)])
|
||||
async def get_user_credits(
|
||||
user_id: Annotated[str, Depends(get_user_id)]
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
) -> dict[str, int]:
|
||||
# Credits can go negative, so ensure it's at least 0 for user to see.
|
||||
return {"credits": max(await _user_credit_model.get_or_refill_credit(user_id), 0)}
|
||||
|
||||
709
autogpt_platform/backend/backend/server/v2/store/db.py
Normal file
709
autogpt_platform/backend/backend/server/v2/store/db.py
Normal file
@@ -0,0 +1,709 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
import random
|
||||
import prisma.enums
|
||||
import prisma.errors
|
||||
import prisma.models
|
||||
import prisma.types
|
||||
|
||||
import backend.server.v2.store.exceptions
|
||||
import backend.server.v2.store.model
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_store_agents(
|
||||
featured: bool = False,
|
||||
creator: str | None = None,
|
||||
sorted_by: str | None = None,
|
||||
search_query: str | None = None,
|
||||
category: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> backend.server.v2.store.model.StoreAgentsResponse:
|
||||
logger.debug(
|
||||
f"Getting store agents. featured={featured}, creator={creator}, sorted_by={sorted_by}, search={search_query}, category={category}, page={page}"
|
||||
)
|
||||
|
||||
where_clause = {}
|
||||
if featured:
|
||||
where_clause["featured"] = featured
|
||||
if creator:
|
||||
where_clause["creator_username"] = creator
|
||||
if category:
|
||||
where_clause["categories"] = {"has": category}
|
||||
if search_query:
|
||||
where_clause["OR"] = [
|
||||
{"agent_name": {"contains": search_query, "mode": "insensitive"}},
|
||||
{"description": {"contains": search_query, "mode": "insensitive"}},
|
||||
]
|
||||
|
||||
order_by = []
|
||||
if sorted_by == "rating":
|
||||
order_by.append({"rating": "desc"})
|
||||
elif sorted_by == "runs":
|
||||
order_by.append({"runs": "desc"})
|
||||
elif sorted_by == "name":
|
||||
order_by.append({"agent_name": "asc"})
|
||||
|
||||
try:
|
||||
agents = await prisma.models.StoreAgent.prisma().find_many(
|
||||
where=prisma.types.StoreAgentWhereInput(**where_clause),
|
||||
order=order_by,
|
||||
skip=(page - 1) * page_size,
|
||||
take=page_size,
|
||||
)
|
||||
|
||||
total = await prisma.models.StoreAgent.prisma().count(
|
||||
where=prisma.types.StoreAgentWhereInput(**where_clause)
|
||||
)
|
||||
total_pages = (total + page_size - 1) // page_size
|
||||
|
||||
store_agents = [
|
||||
backend.server.v2.store.model.StoreAgent(
|
||||
slug=agent.slug,
|
||||
agent_name=agent.agent_name,
|
||||
agent_image=agent.agent_image[0] if agent.agent_image else "",
|
||||
creator=agent.creator_username,
|
||||
creator_avatar=agent.creator_avatar,
|
||||
sub_heading=agent.sub_heading,
|
||||
description=agent.description,
|
||||
runs=agent.runs,
|
||||
rating=agent.rating,
|
||||
)
|
||||
for agent in agents
|
||||
]
|
||||
|
||||
logger.debug(f"Found {len(store_agents)} agents")
|
||||
return backend.server.v2.store.model.StoreAgentsResponse(
|
||||
agents=store_agents,
|
||||
pagination=backend.server.v2.store.model.Pagination(
|
||||
current_page=page,
|
||||
total_items=total,
|
||||
total_pages=total_pages,
|
||||
page_size=page_size,
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting store agents: {str(e)}")
|
||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Failed to fetch store agents"
|
||||
) from e
|
||||
|
||||
|
||||
async def get_store_agent_details(
|
||||
username: str, agent_name: str
|
||||
) -> backend.server.v2.store.model.StoreAgentDetails:
|
||||
logger.debug(f"Getting store agent details for {username}/{agent_name}")
|
||||
|
||||
try:
|
||||
agent = await prisma.models.StoreAgent.prisma().find_first(
|
||||
where={"creator_username": username, "slug": agent_name}
|
||||
)
|
||||
|
||||
if not agent:
|
||||
logger.warning(f"Agent not found: {username}/{agent_name}")
|
||||
raise backend.server.v2.store.exceptions.AgentNotFoundError(
|
||||
f"Agent {username}/{agent_name} not found"
|
||||
)
|
||||
|
||||
logger.debug(f"Found agent details for {username}/{agent_name}")
|
||||
return backend.server.v2.store.model.StoreAgentDetails(
|
||||
store_listing_version_id=agent.storeListingVersionId,
|
||||
slug=agent.slug,
|
||||
agent_name=agent.agent_name,
|
||||
agent_video=agent.agent_video or "",
|
||||
agent_image=agent.agent_image,
|
||||
creator=agent.creator_username,
|
||||
creator_avatar=agent.creator_avatar,
|
||||
sub_heading=agent.sub_heading,
|
||||
description=agent.description,
|
||||
categories=agent.categories,
|
||||
runs=agent.runs,
|
||||
rating=agent.rating,
|
||||
versions=agent.versions,
|
||||
last_updated=agent.updated_at,
|
||||
)
|
||||
except backend.server.v2.store.exceptions.AgentNotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting store agent details: {str(e)}")
|
||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Failed to fetch agent details"
|
||||
) from e
|
||||
|
||||
|
||||
async def get_store_creators(
|
||||
featured: bool = False,
|
||||
search_query: str | None = None,
|
||||
sorted_by: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> backend.server.v2.store.model.CreatorsResponse:
|
||||
logger.debug(
|
||||
f"Getting store creators. featured={featured}, search={search_query}, sorted_by={sorted_by}, page={page}"
|
||||
)
|
||||
|
||||
# Build where clause
|
||||
where = {}
|
||||
|
||||
# Add search filter if provided
|
||||
if search_query:
|
||||
where["OR"] = [
|
||||
{"username": {"contains": search_query, "mode": "insensitive"}},
|
||||
{"name": {"contains": search_query, "mode": "insensitive"}},
|
||||
{"description": {"contains": search_query, "mode": "insensitive"}},
|
||||
]
|
||||
|
||||
try:
|
||||
# Get total count for pagination
|
||||
total = await prisma.models.Creator.prisma().count(
|
||||
where=prisma.types.CreatorWhereInput(**where)
|
||||
)
|
||||
total_pages = (total + page_size - 1) // page_size
|
||||
|
||||
# Add pagination
|
||||
skip = (page - 1) * page_size
|
||||
take = page_size
|
||||
|
||||
# Add sorting
|
||||
order = []
|
||||
if sorted_by == "agent_rating":
|
||||
order.append({"agent_rating": "desc"})
|
||||
elif sorted_by == "agent_runs":
|
||||
order.append({"agent_runs": "desc"})
|
||||
elif sorted_by == "num_agents":
|
||||
order.append({"num_agents": "desc"})
|
||||
else:
|
||||
order.append({"username": "asc"})
|
||||
|
||||
# Execute query
|
||||
creators = await prisma.models.Creator.prisma().find_many(
|
||||
where=prisma.types.CreatorWhereInput(**where),
|
||||
skip=skip,
|
||||
take=take,
|
||||
order=order,
|
||||
)
|
||||
|
||||
# Convert to response model
|
||||
creator_models = [
|
||||
backend.server.v2.store.model.Creator(
|
||||
username=creator.username,
|
||||
name=creator.name,
|
||||
description=creator.description,
|
||||
avatar_url=creator.avatar_url,
|
||||
num_agents=creator.num_agents,
|
||||
agent_rating=creator.agent_rating,
|
||||
agent_runs=creator.agent_runs,
|
||||
)
|
||||
for creator in creators
|
||||
]
|
||||
|
||||
logger.debug(f"Found {len(creator_models)} creators")
|
||||
return backend.server.v2.store.model.CreatorsResponse(
|
||||
creators=creator_models,
|
||||
pagination=backend.server.v2.store.model.Pagination(
|
||||
current_page=page,
|
||||
total_items=total,
|
||||
total_pages=total_pages,
|
||||
page_size=page_size,
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting store creators: {str(e)}")
|
||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Failed to fetch store creators"
|
||||
) from e
|
||||
|
||||
|
||||
async def get_store_creator_details(
|
||||
username: str,
|
||||
) -> backend.server.v2.store.model.CreatorDetails:
|
||||
logger.debug(f"Getting store creator details for {username}")
|
||||
|
||||
try:
|
||||
# Query creator details from database
|
||||
creator = await prisma.models.Creator.prisma().find_unique(
|
||||
where={"username": username}
|
||||
)
|
||||
|
||||
if not creator:
|
||||
logger.warning(f"Creator not found: {username}")
|
||||
raise backend.server.v2.store.exceptions.CreatorNotFoundError(
|
||||
f"Creator {username} not found"
|
||||
)
|
||||
|
||||
logger.debug(f"Found creator details for {username}")
|
||||
return backend.server.v2.store.model.CreatorDetails(
|
||||
name=creator.name,
|
||||
username=creator.username,
|
||||
description=creator.description,
|
||||
links=creator.links,
|
||||
avatar_url=creator.avatar_url,
|
||||
agent_rating=creator.agent_rating,
|
||||
agent_runs=creator.agent_runs,
|
||||
top_categories=creator.top_categories,
|
||||
)
|
||||
except backend.server.v2.store.exceptions.CreatorNotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting store creator details: {str(e)}")
|
||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Failed to fetch creator details"
|
||||
) from e
|
||||
|
||||
|
||||
async def get_store_submissions(
|
||||
user_id: str, page: int = 1, page_size: int = 20
|
||||
) -> backend.server.v2.store.model.StoreSubmissionsResponse:
|
||||
logger.debug(f"Getting store submissions for user {user_id}, page={page}")
|
||||
|
||||
try:
|
||||
# Calculate pagination values
|
||||
skip = (page - 1) * page_size
|
||||
|
||||
where = prisma.types.StoreSubmissionWhereInput(user_id=user_id)
|
||||
# Query submissions from database
|
||||
submissions = await prisma.models.StoreSubmission.prisma().find_many(
|
||||
where=where, skip=skip, take=page_size, order=[{"date_submitted": "desc"}]
|
||||
)
|
||||
|
||||
# Get total count for pagination
|
||||
total = await prisma.models.StoreSubmission.prisma().count(where=where)
|
||||
|
||||
total_pages = (total + page_size - 1) // page_size
|
||||
|
||||
# Convert to response models
|
||||
submission_models = [
|
||||
backend.server.v2.store.model.StoreSubmission(
|
||||
agent_id=sub.agent_id,
|
||||
agent_version=sub.agent_version,
|
||||
name=sub.name,
|
||||
sub_heading=sub.sub_heading,
|
||||
slug=sub.slug,
|
||||
description=sub.description,
|
||||
image_urls=sub.image_urls or [],
|
||||
date_submitted=sub.date_submitted or datetime.now(),
|
||||
status=sub.status,
|
||||
runs=sub.runs or 0,
|
||||
rating=sub.rating or 0.0,
|
||||
)
|
||||
for sub in submissions
|
||||
]
|
||||
|
||||
logger.debug(f"Found {len(submission_models)} submissions")
|
||||
return backend.server.v2.store.model.StoreSubmissionsResponse(
|
||||
submissions=submission_models,
|
||||
pagination=backend.server.v2.store.model.Pagination(
|
||||
current_page=page,
|
||||
total_items=total,
|
||||
total_pages=total_pages,
|
||||
page_size=page_size,
|
||||
),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching store submissions: {str(e)}")
|
||||
# Return empty response rather than exposing internal errors
|
||||
return backend.server.v2.store.model.StoreSubmissionsResponse(
|
||||
submissions=[],
|
||||
pagination=backend.server.v2.store.model.Pagination(
|
||||
current_page=page,
|
||||
total_items=0,
|
||||
total_pages=0,
|
||||
page_size=page_size,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def delete_store_submission(
|
||||
user_id: str,
|
||||
submission_id: str,
|
||||
) -> bool:
|
||||
"""
|
||||
Delete a store listing submission.
|
||||
|
||||
Args:
|
||||
user_id: ID of the authenticated user
|
||||
submission_id: ID of the submission to be deleted
|
||||
|
||||
Returns:
|
||||
bool: True if the submission was successfully deleted, False otherwise
|
||||
"""
|
||||
logger.debug(f"Deleting store submission {submission_id} for user {user_id}")
|
||||
|
||||
try:
|
||||
# Verify the submission belongs to this user
|
||||
submission = await prisma.models.StoreListing.prisma().find_first(
|
||||
where={"agentId": submission_id, "owningUserId": user_id}
|
||||
)
|
||||
|
||||
if not submission:
|
||||
logger.warning(f"Submission not found for user {user_id}: {submission_id}")
|
||||
raise backend.server.v2.store.exceptions.SubmissionNotFoundError(
|
||||
f"Submission not found for this user. User ID: {user_id}, Submission ID: {submission_id}"
|
||||
)
|
||||
|
||||
# Delete the submission
|
||||
await prisma.models.StoreListing.prisma().delete(
|
||||
where=prisma.types.StoreListingWhereUniqueInput(id=submission.id)
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"Successfully deleted submission {submission_id} for user {user_id}"
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting store submission: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
async def create_store_submission(
|
||||
user_id: str,
|
||||
agent_id: str,
|
||||
agent_version: int,
|
||||
slug: str,
|
||||
name: str,
|
||||
video_url: str | None = None,
|
||||
image_urls: list[str] = [],
|
||||
description: str = "",
|
||||
sub_heading: str = "",
|
||||
categories: list[str] = [],
|
||||
) -> backend.server.v2.store.model.StoreSubmission:
|
||||
"""
|
||||
Create a new store listing submission.
|
||||
|
||||
Args:
|
||||
user_id: ID of the authenticated user submitting the listing
|
||||
agent_id: ID of the agent being submitted
|
||||
agent_version: Version of the agent being submitted
|
||||
slug: URL slug for the listing
|
||||
name: Name of the agent
|
||||
video_url: Optional URL to video demo
|
||||
image_urls: List of image URLs for the listing
|
||||
description: Description of the agent
|
||||
categories: List of categories for the agent
|
||||
|
||||
Returns:
|
||||
StoreSubmission: The created store submission
|
||||
"""
|
||||
logger.debug(
|
||||
f"Creating store submission for user {user_id}, agent {agent_id} v{agent_version}"
|
||||
)
|
||||
|
||||
try:
|
||||
# First verify the agent belongs to this user
|
||||
agent = await prisma.models.AgentGraph.prisma().find_first(
|
||||
where=prisma.types.AgentGraphWhereInput(
|
||||
id=agent_id, version=agent_version, userId=user_id
|
||||
)
|
||||
)
|
||||
|
||||
if not agent:
|
||||
logger.warning(
|
||||
f"Agent not found for user {user_id}: {agent_id} v{agent_version}"
|
||||
)
|
||||
raise backend.server.v2.store.exceptions.AgentNotFoundError(
|
||||
f"Agent not found for this user. User ID: {user_id}, Agent ID: {agent_id}, Version: {agent_version}"
|
||||
)
|
||||
|
||||
listing = await prisma.models.StoreListing.prisma().find_first(
|
||||
where=prisma.types.StoreListingWhereInput(
|
||||
agentId=agent_id, owningUserId=user_id
|
||||
)
|
||||
)
|
||||
if listing is not None:
|
||||
logger.warning(f"Listing already exists for agent {agent_id}")
|
||||
raise backend.server.v2.store.exceptions.ListingExistsError(
|
||||
"Listing already exists for this agent"
|
||||
)
|
||||
|
||||
# Create the store listing
|
||||
listing = await prisma.models.StoreListing.prisma().create(
|
||||
data={
|
||||
"agentId": agent_id,
|
||||
"agentVersion": agent_version,
|
||||
"owningUserId": user_id,
|
||||
"createdAt": datetime.now(),
|
||||
"StoreListingVersions": {
|
||||
"create": {
|
||||
"agentId": agent_id,
|
||||
"agentVersion": agent_version,
|
||||
"slug": slug,
|
||||
"name": name,
|
||||
"videoUrl": video_url,
|
||||
"imageUrls": image_urls,
|
||||
"description": description,
|
||||
"categories": categories,
|
||||
"subHeading": sub_heading,
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
logger.debug(f"Created store listing for agent {agent_id}")
|
||||
# Return submission details
|
||||
return backend.server.v2.store.model.StoreSubmission(
|
||||
agent_id=agent_id,
|
||||
agent_version=agent_version,
|
||||
name=name,
|
||||
slug=slug,
|
||||
sub_heading=sub_heading,
|
||||
description=description,
|
||||
image_urls=image_urls,
|
||||
date_submitted=listing.createdAt,
|
||||
status=prisma.enums.SubmissionStatus.PENDING,
|
||||
runs=0,
|
||||
rating=0.0,
|
||||
)
|
||||
|
||||
except (
|
||||
backend.server.v2.store.exceptions.AgentNotFoundError,
|
||||
backend.server.v2.store.exceptions.ListingExistsError,
|
||||
):
|
||||
raise
|
||||
except prisma.errors.PrismaError as e:
|
||||
logger.error(f"Database error creating store submission: {str(e)}")
|
||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Failed to create store submission"
|
||||
) from e
|
||||
|
||||
|
||||
async def create_store_review(
|
||||
user_id: str,
|
||||
store_listing_version_id: str,
|
||||
score: int,
|
||||
comments: str | None = None,
|
||||
) -> backend.server.v2.store.model.StoreReview:
|
||||
try:
|
||||
review = await prisma.models.StoreListingReview.prisma().upsert(
|
||||
where={
|
||||
"storeListingVersionId_reviewByUserId": {
|
||||
"storeListingVersionId": store_listing_version_id,
|
||||
"reviewByUserId": user_id,
|
||||
}
|
||||
},
|
||||
data={
|
||||
"create": {
|
||||
"reviewByUserId": user_id,
|
||||
"storeListingVersionId": store_listing_version_id,
|
||||
"score": score,
|
||||
"comments": comments,
|
||||
},
|
||||
"update": {
|
||||
"score": score,
|
||||
"comments": comments,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return backend.server.v2.store.model.StoreReview(
|
||||
score=review.score,
|
||||
comments=review.comments,
|
||||
)
|
||||
|
||||
except prisma.errors.PrismaError as e:
|
||||
logger.error(f"Database error creating store review: {str(e)}")
|
||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Failed to create store review"
|
||||
) from e
|
||||
|
||||
|
||||
async def get_user_profile(
|
||||
user_id: str,
|
||||
) -> backend.server.v2.store.model.ProfileDetails:
|
||||
logger.debug(f"Getting user profile for {user_id}")
|
||||
|
||||
try:
|
||||
profile = await prisma.models.Profile.prisma().find_first(
|
||||
where={"userId": user_id} # type: ignore
|
||||
)
|
||||
|
||||
if not profile:
|
||||
logger.warning(f"Profile not found for user {user_id}")
|
||||
await prisma.models.Profile.prisma().create(
|
||||
data=prisma.types.ProfileCreateInput(
|
||||
userId=user_id,
|
||||
name="No Profile Data",
|
||||
username=f"{random.choice(['happy', 'clever', 'swift', 'bright', 'wise'])}-{random.choice(['fox', 'wolf', 'bear', 'eagle', 'owl'])}_{random.randint(1000,9999)}",
|
||||
description="No Profile Data",
|
||||
links=[],
|
||||
avatarUrl="",
|
||||
)
|
||||
)
|
||||
return backend.server.v2.store.model.ProfileDetails(
|
||||
name="No Profile Data",
|
||||
username="No Profile Data",
|
||||
description="No Profile Data",
|
||||
links=[],
|
||||
avatar_url="",
|
||||
)
|
||||
|
||||
return backend.server.v2.store.model.ProfileDetails(
|
||||
name=profile.name,
|
||||
username=profile.username,
|
||||
description=profile.description,
|
||||
links=profile.links,
|
||||
avatar_url=profile.avatarUrl,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user profile: {str(e)}")
|
||||
return backend.server.v2.store.model.ProfileDetails(
|
||||
name="No Profile Data",
|
||||
username="No Profile Data",
|
||||
description="No Profile Data",
|
||||
links=[],
|
||||
avatar_url="",
|
||||
)
|
||||
|
||||
|
||||
async def update_or_create_profile(
|
||||
user_id: str, profile: backend.server.v2.store.model.Profile
|
||||
) -> backend.server.v2.store.model.CreatorDetails:
|
||||
"""
|
||||
Update the store profile for a user. Creates a new profile if one doesn't exist.
|
||||
Only allows updating if the user_id matches the owning user.
|
||||
|
||||
Args:
|
||||
user_id: ID of the authenticated user
|
||||
profile: Updated profile details
|
||||
|
||||
Returns:
|
||||
CreatorDetails: The updated profile
|
||||
|
||||
Raises:
|
||||
HTTPException: If user is not authorized to update this profile
|
||||
"""
|
||||
logger.debug(f"Updating profile for user {user_id}")
|
||||
|
||||
try:
|
||||
# Check if profile exists for user
|
||||
existing_profile = await prisma.models.Profile.prisma().find_first(
|
||||
where={"userId": user_id}
|
||||
)
|
||||
|
||||
# If no profile exists, create a new one
|
||||
if not existing_profile:
|
||||
logger.debug(f"Creating new profile for user {user_id}")
|
||||
# Create new profile since one doesn't exist
|
||||
new_profile = await prisma.models.Profile.prisma().create(
|
||||
data={
|
||||
"userId": user_id,
|
||||
"name": profile.name,
|
||||
"username": profile.username,
|
||||
"description": profile.description,
|
||||
"links": profile.links,
|
||||
"avatarUrl": profile.avatar_url,
|
||||
}
|
||||
)
|
||||
|
||||
return backend.server.v2.store.model.CreatorDetails(
|
||||
name=new_profile.name,
|
||||
username=new_profile.username,
|
||||
description=new_profile.description,
|
||||
links=new_profile.links,
|
||||
avatar_url=new_profile.avatarUrl or "",
|
||||
agent_rating=0.0,
|
||||
agent_runs=0,
|
||||
top_categories=[],
|
||||
)
|
||||
else:
|
||||
logger.debug(f"Updating existing profile for user {user_id}")
|
||||
# Update the existing profile
|
||||
updated_profile = await prisma.models.Profile.prisma().update(
|
||||
where={"id": existing_profile.id},
|
||||
data=prisma.types.ProfileUpdateInput(
|
||||
name=profile.name,
|
||||
username=profile.username,
|
||||
description=profile.description,
|
||||
links=profile.links,
|
||||
avatarUrl=profile.avatar_url,
|
||||
),
|
||||
)
|
||||
if updated_profile is None:
|
||||
logger.error(f"Failed to update profile for user {user_id}")
|
||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Failed to update profile"
|
||||
)
|
||||
|
||||
return backend.server.v2.store.model.CreatorDetails(
|
||||
name=updated_profile.name,
|
||||
username=updated_profile.username,
|
||||
description=updated_profile.description,
|
||||
links=updated_profile.links,
|
||||
avatar_url=updated_profile.avatarUrl or "",
|
||||
agent_rating=0.0,
|
||||
agent_runs=0,
|
||||
top_categories=[],
|
||||
)
|
||||
|
||||
except prisma.errors.PrismaError as e:
|
||||
logger.error(f"Database error updating profile: {str(e)}")
|
||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Failed to update profile"
|
||||
) from e
|
||||
|
||||
|
||||
async def get_my_agents(
|
||||
user_id: str,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> backend.server.v2.store.model.MyAgentsResponse:
|
||||
logger.debug(f"Getting my agents for user {user_id}, page={page}")
|
||||
|
||||
try:
|
||||
agents_with_max_version = await prisma.models.AgentGraph.prisma().find_many(
|
||||
where=prisma.types.AgentGraphWhereInput(
|
||||
userId=user_id, StoreListing={"none": {"isDeleted": False}}
|
||||
),
|
||||
order=[{"version": "desc"}],
|
||||
distinct=["id"],
|
||||
skip=(page - 1) * page_size,
|
||||
take=page_size,
|
||||
)
|
||||
|
||||
# store_listings = await prisma.models.StoreListing.prisma().find_many(
|
||||
# where=prisma.types.StoreListingWhereInput(
|
||||
# isDeleted=False,
|
||||
# ),
|
||||
# )
|
||||
|
||||
total = len(
|
||||
await prisma.models.AgentGraph.prisma().find_many(
|
||||
where=prisma.types.AgentGraphWhereInput(
|
||||
userId=user_id, StoreListing={"none": {"isDeleted": False}}
|
||||
),
|
||||
order=[{"version": "desc"}],
|
||||
distinct=["id"],
|
||||
)
|
||||
)
|
||||
|
||||
total_pages = (total + page_size - 1) // page_size
|
||||
|
||||
agents = agents_with_max_version
|
||||
|
||||
my_agents = [
|
||||
backend.server.v2.store.model.MyAgent(
|
||||
agent_id=agent.id,
|
||||
agent_version=agent.version,
|
||||
agent_name=agent.name or "",
|
||||
last_edited=agent.updatedAt or agent.createdAt,
|
||||
)
|
||||
for agent in agents
|
||||
]
|
||||
|
||||
return backend.server.v2.store.model.MyAgentsResponse(
|
||||
agents=my_agents,
|
||||
pagination=backend.server.v2.store.model.Pagination(
|
||||
current_page=page,
|
||||
total_items=total,
|
||||
total_pages=total_pages,
|
||||
page_size=page_size,
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting my agents: {str(e)}")
|
||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Failed to fetch my agents"
|
||||
) from e
|
||||
260
autogpt_platform/backend/backend/server/v2/store/db_test.py
Normal file
260
autogpt_platform/backend/backend/server/v2/store/db_test.py
Normal file
@@ -0,0 +1,260 @@
|
||||
from datetime import datetime
|
||||
|
||||
import prisma.errors
|
||||
import prisma.models
|
||||
import pytest
|
||||
from prisma import Prisma
|
||||
|
||||
import backend.server.v2.store.db as db
|
||||
from backend.server.v2.store.model import Profile
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_prisma():
|
||||
# Don't register client if already registered
|
||||
try:
|
||||
Prisma()
|
||||
except prisma.errors.ClientAlreadyRegisteredError:
|
||||
pass
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_store_agents(mocker):
|
||||
# Mock data
|
||||
mock_agents = [
|
||||
prisma.models.StoreAgent(
|
||||
listing_id="test-id",
|
||||
storeListingVersionId="version123",
|
||||
slug="test-agent",
|
||||
agent_name="Test Agent",
|
||||
agent_video=None,
|
||||
agent_image=["image.jpg"],
|
||||
featured=False,
|
||||
creator_username="creator",
|
||||
creator_avatar="avatar.jpg",
|
||||
sub_heading="Test heading",
|
||||
description="Test description",
|
||||
categories=[],
|
||||
runs=10,
|
||||
rating=4.5,
|
||||
versions=["1.0"],
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
]
|
||||
|
||||
# Mock prisma calls
|
||||
mock_store_agent = mocker.patch("prisma.models.StoreAgent.prisma")
|
||||
mock_store_agent.return_value.find_many = mocker.AsyncMock(return_value=mock_agents)
|
||||
mock_store_agent.return_value.count = mocker.AsyncMock(return_value=1)
|
||||
|
||||
# Call function
|
||||
result = await db.get_store_agents()
|
||||
|
||||
# Verify results
|
||||
assert len(result.agents) == 1
|
||||
assert result.agents[0].slug == "test-agent"
|
||||
assert result.pagination.total_items == 1
|
||||
|
||||
# Verify mocks called correctly
|
||||
mock_store_agent.return_value.find_many.assert_called_once()
|
||||
mock_store_agent.return_value.count.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_store_agent_details(mocker):
|
||||
# Mock data
|
||||
mock_agent = prisma.models.StoreAgent(
|
||||
listing_id="test-id",
|
||||
storeListingVersionId="version123",
|
||||
slug="test-agent",
|
||||
agent_name="Test Agent",
|
||||
agent_video="video.mp4",
|
||||
agent_image=["image.jpg"],
|
||||
featured=False,
|
||||
creator_username="creator",
|
||||
creator_avatar="avatar.jpg",
|
||||
sub_heading="Test heading",
|
||||
description="Test description",
|
||||
categories=["test"],
|
||||
runs=10,
|
||||
rating=4.5,
|
||||
versions=["1.0"],
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
|
||||
# Mock prisma call
|
||||
mock_store_agent = mocker.patch("prisma.models.StoreAgent.prisma")
|
||||
mock_store_agent.return_value.find_first = mocker.AsyncMock(return_value=mock_agent)
|
||||
|
||||
# Call function
|
||||
result = await db.get_store_agent_details("creator", "test-agent")
|
||||
|
||||
# Verify results
|
||||
assert result.slug == "test-agent"
|
||||
assert result.agent_name == "Test Agent"
|
||||
|
||||
# Verify mock called correctly
|
||||
mock_store_agent.return_value.find_first.assert_called_once_with(
|
||||
where={"creator_username": "creator", "slug": "test-agent"}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_store_creator_details(mocker):
|
||||
# Mock data
|
||||
mock_creator_data = prisma.models.Creator(
|
||||
name="Test Creator",
|
||||
username="creator",
|
||||
description="Test description",
|
||||
links=["link1"],
|
||||
avatar_url="avatar.jpg",
|
||||
num_agents=1,
|
||||
agent_rating=4.5,
|
||||
agent_runs=10,
|
||||
top_categories=["test"],
|
||||
)
|
||||
|
||||
# Mock prisma call
|
||||
mock_creator = mocker.patch("prisma.models.Creator.prisma")
|
||||
mock_creator.return_value.find_unique = mocker.AsyncMock()
|
||||
# Configure the mock to return values that will pass validation
|
||||
mock_creator.return_value.find_unique.return_value = mock_creator_data
|
||||
|
||||
# Call function
|
||||
result = await db.get_store_creator_details("creator")
|
||||
|
||||
# Verify results
|
||||
assert result.username == "creator"
|
||||
assert result.name == "Test Creator"
|
||||
assert result.description == "Test description"
|
||||
assert result.avatar_url == "avatar.jpg"
|
||||
|
||||
# Verify mock called correctly
|
||||
mock_creator.return_value.find_unique.assert_called_once_with(
|
||||
where={"username": "creator"}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_store_submission(mocker):
|
||||
# Mock data
|
||||
mock_agent = prisma.models.AgentGraph(
|
||||
id="agent-id",
|
||||
version=1,
|
||||
userId="user-id",
|
||||
createdAt=datetime.now(),
|
||||
isActive=True,
|
||||
isTemplate=False,
|
||||
)
|
||||
|
||||
mock_listing = prisma.models.StoreListing(
|
||||
id="listing-id",
|
||||
createdAt=datetime.now(),
|
||||
updatedAt=datetime.now(),
|
||||
isDeleted=False,
|
||||
isApproved=False,
|
||||
agentId="agent-id",
|
||||
agentVersion=1,
|
||||
owningUserId="user-id",
|
||||
)
|
||||
|
||||
# Mock prisma calls
|
||||
mock_agent_graph = mocker.patch("prisma.models.AgentGraph.prisma")
|
||||
mock_agent_graph.return_value.find_first = mocker.AsyncMock(return_value=mock_agent)
|
||||
|
||||
mock_store_listing = mocker.patch("prisma.models.StoreListing.prisma")
|
||||
mock_store_listing.return_value.find_first = mocker.AsyncMock(return_value=None)
|
||||
mock_store_listing.return_value.create = mocker.AsyncMock(return_value=mock_listing)
|
||||
|
||||
# Call function
|
||||
result = await db.create_store_submission(
|
||||
user_id="user-id",
|
||||
agent_id="agent-id",
|
||||
agent_version=1,
|
||||
slug="test-agent",
|
||||
name="Test Agent",
|
||||
description="Test description",
|
||||
)
|
||||
|
||||
# Verify results
|
||||
assert result.name == "Test Agent"
|
||||
assert result.description == "Test description"
|
||||
|
||||
# Verify mocks called correctly
|
||||
mock_agent_graph.return_value.find_first.assert_called_once()
|
||||
mock_store_listing.return_value.find_first.assert_called_once()
|
||||
mock_store_listing.return_value.create.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_profile(mocker):
|
||||
# Mock data
|
||||
mock_profile = prisma.models.Profile(
|
||||
id="profile-id",
|
||||
name="Test Creator",
|
||||
username="creator",
|
||||
description="Test description",
|
||||
links=["link1"],
|
||||
avatarUrl="avatar.jpg",
|
||||
createdAt=datetime.now(),
|
||||
updatedAt=datetime.now(),
|
||||
)
|
||||
|
||||
# Mock prisma calls
|
||||
mock_profile_db = mocker.patch("prisma.models.Profile.prisma")
|
||||
mock_profile_db.return_value.find_first = mocker.AsyncMock(
|
||||
return_value=mock_profile
|
||||
)
|
||||
mock_profile_db.return_value.update = mocker.AsyncMock(return_value=mock_profile)
|
||||
|
||||
# Test data
|
||||
profile = Profile(
|
||||
name="Test Creator",
|
||||
username="creator",
|
||||
description="Test description",
|
||||
links=["link1"],
|
||||
avatar_url="avatar.jpg",
|
||||
)
|
||||
|
||||
# Call function
|
||||
result = await db.update_or_create_profile("user-id", profile)
|
||||
|
||||
# Verify results
|
||||
assert result.username == "creator"
|
||||
assert result.name == "Test Creator"
|
||||
|
||||
# Verify mocks called correctly
|
||||
mock_profile_db.return_value.find_first.assert_called_once()
|
||||
mock_profile_db.return_value.update.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_profile(mocker):
|
||||
# Mock data
|
||||
mock_profile = prisma.models.Profile(
|
||||
id="profile-id",
|
||||
name="No Profile Data",
|
||||
username="testuser",
|
||||
description="Test description",
|
||||
links=["link1", "link2"],
|
||||
avatarUrl="avatar.jpg",
|
||||
createdAt=datetime.now(),
|
||||
updatedAt=datetime.now(),
|
||||
)
|
||||
|
||||
# Mock prisma calls
|
||||
mock_profile_db = mocker.patch("prisma.models.Profile.prisma")
|
||||
mock_profile_db.return_value.find_unique = mocker.AsyncMock(
|
||||
return_value=mock_profile
|
||||
)
|
||||
|
||||
# Call function
|
||||
result = await db.get_user_profile("user-id")
|
||||
|
||||
# Verify results
|
||||
assert result.name == "No Profile Data"
|
||||
assert result.username == "No Profile Data"
|
||||
assert result.description == "No Profile Data"
|
||||
assert result.links == []
|
||||
assert result.avatar_url == ""
|
||||
@@ -0,0 +1,76 @@
|
||||
class MediaUploadError(Exception):
|
||||
"""Base exception for media upload errors"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidFileTypeError(MediaUploadError):
|
||||
"""Raised when file type is not supported"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class FileSizeTooLargeError(MediaUploadError):
|
||||
"""Raised when file size exceeds maximum limit"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class FileReadError(MediaUploadError):
|
||||
"""Raised when there's an error reading the file"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class StorageConfigError(MediaUploadError):
|
||||
"""Raised when storage configuration is invalid"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class StorageUploadError(MediaUploadError):
|
||||
"""Raised when upload to storage fails"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class StoreError(Exception):
|
||||
"""Base exception for store-related errors"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AgentNotFoundError(StoreError):
|
||||
"""Raised when an agent is not found"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CreatorNotFoundError(StoreError):
|
||||
"""Raised when a creator is not found"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ListingExistsError(StoreError):
|
||||
"""Raised when trying to create a listing that already exists"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseError(StoreError):
|
||||
"""Raised when there is an error interacting with the database"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ProfileNotFoundError(StoreError):
|
||||
"""Raised when a profile is not found"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class SubmissionNotFoundError(StoreError):
|
||||
"""Raised when a submission is not found"""
|
||||
|
||||
pass
|
||||
101
autogpt_platform/backend/backend/server/v2/store/media.py
Normal file
101
autogpt_platform/backend/backend/server/v2/store/media.py
Normal file
@@ -0,0 +1,101 @@
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import fastapi
|
||||
from google.cloud import storage
|
||||
|
||||
import backend.server.v2.store.exceptions
|
||||
from backend.util.settings import Settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"}
|
||||
ALLOWED_VIDEO_TYPES = {"video/mp4", "video/webm"}
|
||||
MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
|
||||
|
||||
|
||||
async def upload_media(user_id: str, file: fastapi.UploadFile) -> str:
|
||||
settings = Settings()
|
||||
|
||||
# Check required settings first before doing any file processing
|
||||
if (
|
||||
not settings.config.media_gcs_bucket_name
|
||||
or not settings.config.google_application_credentials
|
||||
):
|
||||
logger.error("Missing required GCS settings")
|
||||
raise backend.server.v2.store.exceptions.StorageConfigError(
|
||||
"Missing storage configuration"
|
||||
)
|
||||
|
||||
try:
|
||||
# Validate file type
|
||||
content_type = file.content_type
|
||||
if (
|
||||
content_type not in ALLOWED_IMAGE_TYPES
|
||||
and content_type not in ALLOWED_VIDEO_TYPES
|
||||
):
|
||||
logger.warning(f"Invalid file type attempted: {content_type}")
|
||||
raise backend.server.v2.store.exceptions.InvalidFileTypeError(
|
||||
f"File type not supported. Must be jpeg, png, gif, webp, mp4 or webm. Content type: {content_type}"
|
||||
)
|
||||
|
||||
# Validate file size
|
||||
file_size = 0
|
||||
chunk_size = 8192 # 8KB chunks
|
||||
|
||||
try:
|
||||
while chunk := await file.read(chunk_size):
|
||||
file_size += len(chunk)
|
||||
if file_size > MAX_FILE_SIZE:
|
||||
logger.warning(f"File size too large: {file_size} bytes")
|
||||
raise backend.server.v2.store.exceptions.FileSizeTooLargeError(
|
||||
"File too large. Maximum size is 50MB"
|
||||
)
|
||||
except backend.server.v2.store.exceptions.FileSizeTooLargeError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading file chunks: {str(e)}")
|
||||
raise backend.server.v2.store.exceptions.FileReadError(
|
||||
"Failed to read uploaded file"
|
||||
) from e
|
||||
|
||||
# Reset file pointer
|
||||
await file.seek(0)
|
||||
|
||||
# Generate unique filename
|
||||
filename = file.filename or ""
|
||||
file_ext = os.path.splitext(filename)[1].lower()
|
||||
unique_filename = f"{uuid.uuid4()}{file_ext}"
|
||||
|
||||
# Construct storage path
|
||||
media_type = "images" if content_type in ALLOWED_IMAGE_TYPES else "videos"
|
||||
storage_path = f"users/{user_id}/{media_type}/{unique_filename}"
|
||||
|
||||
try:
|
||||
storage_client = storage.Client()
|
||||
bucket = storage_client.bucket(settings.config.media_gcs_bucket_name)
|
||||
blob = bucket.blob(storage_path)
|
||||
blob.content_type = content_type
|
||||
|
||||
file_bytes = await file.read()
|
||||
blob.upload_from_string(file_bytes, content_type=content_type)
|
||||
|
||||
public_url = blob.public_url
|
||||
|
||||
logger.info(f"Successfully uploaded file to: {storage_path}")
|
||||
return public_url
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"GCS storage error: {str(e)}")
|
||||
raise backend.server.v2.store.exceptions.StorageUploadError(
|
||||
"Failed to upload file to storage"
|
||||
) from e
|
||||
|
||||
except backend.server.v2.store.exceptions.MediaUploadError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error in upload_media")
|
||||
raise backend.server.v2.store.exceptions.MediaUploadError(
|
||||
"Unexpected error during media upload"
|
||||
) from e
|
||||
107
autogpt_platform/backend/backend/server/v2/store/media_test.py
Normal file
107
autogpt_platform/backend/backend/server/v2/store/media_test.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import io
|
||||
import unittest.mock
|
||||
|
||||
import fastapi
|
||||
import pytest
|
||||
import starlette.datastructures
|
||||
|
||||
import backend.server.v2.store.exceptions
|
||||
import backend.server.v2.store.media
|
||||
from backend.util.settings import Settings
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_settings(monkeypatch):
|
||||
settings = Settings()
|
||||
settings.config.media_gcs_bucket_name = "test-bucket"
|
||||
settings.config.google_application_credentials = "test-credentials"
|
||||
monkeypatch.setattr("backend.server.v2.store.media.Settings", lambda: settings)
|
||||
return settings
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_storage_client(mocker):
|
||||
mock_client = unittest.mock.MagicMock()
|
||||
mock_bucket = unittest.mock.MagicMock()
|
||||
mock_blob = unittest.mock.MagicMock()
|
||||
|
||||
mock_client.bucket.return_value = mock_bucket
|
||||
mock_bucket.blob.return_value = mock_blob
|
||||
mock_blob.public_url = "http://test-url/media/test.jpg"
|
||||
|
||||
mocker.patch("google.cloud.storage.Client", return_value=mock_client)
|
||||
|
||||
return mock_client
|
||||
|
||||
|
||||
async def test_upload_media_success(mock_settings, mock_storage_client):
|
||||
test_file = fastapi.UploadFile(
|
||||
filename="test.jpeg",
|
||||
file=io.BytesIO(b"test data"),
|
||||
headers=starlette.datastructures.Headers({"content-type": "image/jpeg"}),
|
||||
)
|
||||
|
||||
result = await backend.server.v2.store.media.upload_media("test-user", test_file)
|
||||
|
||||
assert result == "http://test-url/media/test.jpg"
|
||||
mock_bucket = mock_storage_client.bucket.return_value
|
||||
mock_blob = mock_bucket.blob.return_value
|
||||
mock_blob.upload_from_string.assert_called_once()
|
||||
|
||||
|
||||
async def test_upload_media_invalid_type(mock_settings, mock_storage_client):
|
||||
test_file = fastapi.UploadFile(
|
||||
filename="test.txt",
|
||||
file=io.BytesIO(b"test data"),
|
||||
headers=starlette.datastructures.Headers({"content-type": "text/plain"}),
|
||||
)
|
||||
|
||||
with pytest.raises(backend.server.v2.store.exceptions.InvalidFileTypeError):
|
||||
await backend.server.v2.store.media.upload_media("test-user", test_file)
|
||||
|
||||
mock_bucket = mock_storage_client.bucket.return_value
|
||||
mock_blob = mock_bucket.blob.return_value
|
||||
mock_blob.upload_from_string.assert_not_called()
|
||||
|
||||
|
||||
async def test_upload_media_missing_credentials(monkeypatch):
|
||||
settings = Settings()
|
||||
settings.config.media_gcs_bucket_name = ""
|
||||
settings.config.google_application_credentials = ""
|
||||
monkeypatch.setattr("backend.server.v2.store.media.Settings", lambda: settings)
|
||||
|
||||
test_file = fastapi.UploadFile(
|
||||
filename="test.jpeg",
|
||||
file=io.BytesIO(b"test data"),
|
||||
headers=starlette.datastructures.Headers({"content-type": "image/jpeg"}),
|
||||
)
|
||||
|
||||
with pytest.raises(backend.server.v2.store.exceptions.StorageConfigError):
|
||||
await backend.server.v2.store.media.upload_media("test-user", test_file)
|
||||
|
||||
|
||||
async def test_upload_media_video_type(mock_settings, mock_storage_client):
|
||||
test_file = fastapi.UploadFile(
|
||||
filename="test.mp4",
|
||||
file=io.BytesIO(b"test video data"),
|
||||
headers=starlette.datastructures.Headers({"content-type": "video/mp4"}),
|
||||
)
|
||||
|
||||
result = await backend.server.v2.store.media.upload_media("test-user", test_file)
|
||||
|
||||
assert result == "http://test-url/media/test.jpg"
|
||||
mock_bucket = mock_storage_client.bucket.return_value
|
||||
mock_blob = mock_bucket.blob.return_value
|
||||
mock_blob.upload_from_string.assert_called_once()
|
||||
|
||||
|
||||
async def test_upload_media_file_too_large(mock_settings, mock_storage_client):
|
||||
large_data = b"x" * (50 * 1024 * 1024 + 1) # 50MB + 1 byte
|
||||
test_file = fastapi.UploadFile(
|
||||
filename="test.jpeg",
|
||||
file=io.BytesIO(large_data),
|
||||
headers=starlette.datastructures.Headers({"content-type": "image/jpeg"}),
|
||||
)
|
||||
|
||||
with pytest.raises(backend.server.v2.store.exceptions.FileSizeTooLargeError):
|
||||
await backend.server.v2.store.media.upload_media("test-user", test_file)
|
||||
150
autogpt_platform/backend/backend/server/v2/store/model.py
Normal file
150
autogpt_platform/backend/backend/server/v2/store/model.py
Normal file
@@ -0,0 +1,150 @@
|
||||
import datetime
|
||||
from typing import List
|
||||
|
||||
import prisma.enums
|
||||
import pydantic
|
||||
|
||||
|
||||
class Pagination(pydantic.BaseModel):
|
||||
total_items: int = pydantic.Field(
|
||||
description="Total number of items.", examples=[42]
|
||||
)
|
||||
total_pages: int = pydantic.Field(
|
||||
description="Total number of pages.", examples=[97]
|
||||
)
|
||||
current_page: int = pydantic.Field(
|
||||
description="Current_page page number.", examples=[1]
|
||||
)
|
||||
page_size: int = pydantic.Field(
|
||||
description="Number of items per page.", examples=[25]
|
||||
)
|
||||
|
||||
|
||||
class MyAgent(pydantic.BaseModel):
|
||||
agent_id: str
|
||||
agent_version: int
|
||||
agent_name: str
|
||||
last_edited: datetime.datetime
|
||||
|
||||
|
||||
class MyAgentsResponse(pydantic.BaseModel):
|
||||
agents: list[MyAgent]
|
||||
pagination: Pagination
|
||||
|
||||
|
||||
class StoreAgent(pydantic.BaseModel):
|
||||
slug: str
|
||||
agent_name: str
|
||||
agent_image: str
|
||||
creator: str
|
||||
creator_avatar: str
|
||||
sub_heading: str
|
||||
description: str
|
||||
runs: int
|
||||
rating: float
|
||||
|
||||
|
||||
class StoreAgentsResponse(pydantic.BaseModel):
|
||||
agents: list[StoreAgent]
|
||||
pagination: Pagination
|
||||
|
||||
|
||||
class StoreAgentDetails(pydantic.BaseModel):
|
||||
store_listing_version_id: str
|
||||
slug: str
|
||||
agent_name: str
|
||||
agent_video: str
|
||||
agent_image: list[str]
|
||||
creator: str
|
||||
creator_avatar: str
|
||||
sub_heading: str
|
||||
description: str
|
||||
categories: list[str]
|
||||
runs: int
|
||||
rating: float
|
||||
versions: list[str]
|
||||
last_updated: datetime.datetime
|
||||
|
||||
|
||||
class Creator(pydantic.BaseModel):
|
||||
name: str
|
||||
username: str
|
||||
description: str
|
||||
avatar_url: str
|
||||
num_agents: int
|
||||
agent_rating: float
|
||||
agent_runs: int
|
||||
|
||||
|
||||
class CreatorsResponse(pydantic.BaseModel):
|
||||
creators: List[Creator]
|
||||
pagination: Pagination
|
||||
|
||||
|
||||
class CreatorDetails(pydantic.BaseModel):
|
||||
name: str
|
||||
username: str
|
||||
description: str
|
||||
links: list[str]
|
||||
avatar_url: str
|
||||
agent_rating: float
|
||||
agent_runs: int
|
||||
top_categories: list[str]
|
||||
|
||||
|
||||
class Profile(pydantic.BaseModel):
|
||||
name: str
|
||||
username: str
|
||||
description: str
|
||||
links: list[str]
|
||||
avatar_url: str
|
||||
|
||||
|
||||
class StoreSubmission(pydantic.BaseModel):
|
||||
agent_id: str
|
||||
agent_version: int
|
||||
name: str
|
||||
sub_heading: str
|
||||
slug: str
|
||||
description: str
|
||||
image_urls: list[str]
|
||||
date_submitted: datetime.datetime
|
||||
status: prisma.enums.SubmissionStatus
|
||||
runs: int
|
||||
rating: float
|
||||
|
||||
|
||||
class StoreSubmissionsResponse(pydantic.BaseModel):
|
||||
submissions: list[StoreSubmission]
|
||||
pagination: Pagination
|
||||
|
||||
|
||||
class StoreSubmissionRequest(pydantic.BaseModel):
|
||||
agent_id: str
|
||||
agent_version: int
|
||||
slug: str
|
||||
name: str
|
||||
sub_heading: str
|
||||
video_url: str | None = None
|
||||
image_urls: list[str] = []
|
||||
description: str = ""
|
||||
categories: list[str] = []
|
||||
|
||||
|
||||
class ProfileDetails(pydantic.BaseModel):
|
||||
name: str
|
||||
username: str
|
||||
description: str
|
||||
links: list[str]
|
||||
avatar_url: str | None = None
|
||||
|
||||
|
||||
class StoreReview(pydantic.BaseModel):
|
||||
score: int
|
||||
comments: str | None = None
|
||||
|
||||
|
||||
class StoreReviewCreate(pydantic.BaseModel):
|
||||
store_listing_version_id: str
|
||||
score: int
|
||||
comments: str | None = None
|
||||
193
autogpt_platform/backend/backend/server/v2/store/model_test.py
Normal file
193
autogpt_platform/backend/backend/server/v2/store/model_test.py
Normal file
@@ -0,0 +1,193 @@
|
||||
import datetime
|
||||
|
||||
import prisma.enums
|
||||
|
||||
import backend.server.v2.store.model
|
||||
|
||||
|
||||
def test_pagination():
|
||||
pagination = backend.server.v2.store.model.Pagination(
|
||||
total_items=100, total_pages=5, current_page=2, page_size=20
|
||||
)
|
||||
assert pagination.total_items == 100
|
||||
assert pagination.total_pages == 5
|
||||
assert pagination.current_page == 2
|
||||
assert pagination.page_size == 20
|
||||
|
||||
|
||||
def test_store_agent():
|
||||
agent = backend.server.v2.store.model.StoreAgent(
|
||||
slug="test-agent",
|
||||
agent_name="Test Agent",
|
||||
agent_image="test.jpg",
|
||||
creator="creator1",
|
||||
creator_avatar="avatar.jpg",
|
||||
sub_heading="Test subheading",
|
||||
description="Test description",
|
||||
runs=50,
|
||||
rating=4.5,
|
||||
)
|
||||
assert agent.slug == "test-agent"
|
||||
assert agent.agent_name == "Test Agent"
|
||||
assert agent.runs == 50
|
||||
assert agent.rating == 4.5
|
||||
|
||||
|
||||
def test_store_agents_response():
|
||||
response = backend.server.v2.store.model.StoreAgentsResponse(
|
||||
agents=[
|
||||
backend.server.v2.store.model.StoreAgent(
|
||||
slug="test-agent",
|
||||
agent_name="Test Agent",
|
||||
agent_image="test.jpg",
|
||||
creator="creator1",
|
||||
creator_avatar="avatar.jpg",
|
||||
sub_heading="Test subheading",
|
||||
description="Test description",
|
||||
runs=50,
|
||||
rating=4.5,
|
||||
)
|
||||
],
|
||||
pagination=backend.server.v2.store.model.Pagination(
|
||||
total_items=1, total_pages=1, current_page=1, page_size=20
|
||||
),
|
||||
)
|
||||
assert len(response.agents) == 1
|
||||
assert response.pagination.total_items == 1
|
||||
|
||||
|
||||
def test_store_agent_details():
|
||||
details = backend.server.v2.store.model.StoreAgentDetails(
|
||||
store_listing_version_id="version123",
|
||||
slug="test-agent",
|
||||
agent_name="Test Agent",
|
||||
agent_video="video.mp4",
|
||||
agent_image=["image1.jpg", "image2.jpg"],
|
||||
creator="creator1",
|
||||
creator_avatar="avatar.jpg",
|
||||
sub_heading="Test subheading",
|
||||
description="Test description",
|
||||
categories=["cat1", "cat2"],
|
||||
runs=50,
|
||||
rating=4.5,
|
||||
versions=["1.0", "2.0"],
|
||||
last_updated=datetime.datetime.now(),
|
||||
)
|
||||
assert details.slug == "test-agent"
|
||||
assert len(details.agent_image) == 2
|
||||
assert len(details.categories) == 2
|
||||
assert len(details.versions) == 2
|
||||
|
||||
|
||||
def test_creator():
|
||||
creator = backend.server.v2.store.model.Creator(
|
||||
agent_rating=4.8,
|
||||
agent_runs=1000,
|
||||
name="Test Creator",
|
||||
username="creator1",
|
||||
description="Test description",
|
||||
avatar_url="avatar.jpg",
|
||||
num_agents=5,
|
||||
)
|
||||
assert creator.name == "Test Creator"
|
||||
assert creator.num_agents == 5
|
||||
|
||||
|
||||
def test_creators_response():
|
||||
response = backend.server.v2.store.model.CreatorsResponse(
|
||||
creators=[
|
||||
backend.server.v2.store.model.Creator(
|
||||
agent_rating=4.8,
|
||||
agent_runs=1000,
|
||||
name="Test Creator",
|
||||
username="creator1",
|
||||
description="Test description",
|
||||
avatar_url="avatar.jpg",
|
||||
num_agents=5,
|
||||
)
|
||||
],
|
||||
pagination=backend.server.v2.store.model.Pagination(
|
||||
total_items=1, total_pages=1, current_page=1, page_size=20
|
||||
),
|
||||
)
|
||||
assert len(response.creators) == 1
|
||||
assert response.pagination.total_items == 1
|
||||
|
||||
|
||||
def test_creator_details():
|
||||
details = backend.server.v2.store.model.CreatorDetails(
|
||||
name="Test Creator",
|
||||
username="creator1",
|
||||
description="Test description",
|
||||
links=["link1.com", "link2.com"],
|
||||
avatar_url="avatar.jpg",
|
||||
agent_rating=4.8,
|
||||
agent_runs=1000,
|
||||
top_categories=["cat1", "cat2"],
|
||||
)
|
||||
assert details.name == "Test Creator"
|
||||
assert len(details.links) == 2
|
||||
assert details.agent_rating == 4.8
|
||||
assert len(details.top_categories) == 2
|
||||
|
||||
|
||||
def test_store_submission():
|
||||
submission = backend.server.v2.store.model.StoreSubmission(
|
||||
agent_id="agent123",
|
||||
agent_version=1,
|
||||
sub_heading="Test subheading",
|
||||
name="Test Agent",
|
||||
slug="test-agent",
|
||||
description="Test description",
|
||||
image_urls=["image1.jpg", "image2.jpg"],
|
||||
date_submitted=datetime.datetime(2023, 1, 1),
|
||||
status=prisma.enums.SubmissionStatus.PENDING,
|
||||
runs=50,
|
||||
rating=4.5,
|
||||
)
|
||||
assert submission.name == "Test Agent"
|
||||
assert len(submission.image_urls) == 2
|
||||
assert submission.status == prisma.enums.SubmissionStatus.PENDING
|
||||
|
||||
|
||||
def test_store_submissions_response():
|
||||
response = backend.server.v2.store.model.StoreSubmissionsResponse(
|
||||
submissions=[
|
||||
backend.server.v2.store.model.StoreSubmission(
|
||||
agent_id="agent123",
|
||||
agent_version=1,
|
||||
sub_heading="Test subheading",
|
||||
name="Test Agent",
|
||||
slug="test-agent",
|
||||
description="Test description",
|
||||
image_urls=["image1.jpg"],
|
||||
date_submitted=datetime.datetime(2023, 1, 1),
|
||||
status=prisma.enums.SubmissionStatus.PENDING,
|
||||
runs=50,
|
||||
rating=4.5,
|
||||
)
|
||||
],
|
||||
pagination=backend.server.v2.store.model.Pagination(
|
||||
total_items=1, total_pages=1, current_page=1, page_size=20
|
||||
),
|
||||
)
|
||||
assert len(response.submissions) == 1
|
||||
assert response.pagination.total_items == 1
|
||||
|
||||
|
||||
def test_store_submission_request():
|
||||
request = backend.server.v2.store.model.StoreSubmissionRequest(
|
||||
agent_id="agent123",
|
||||
agent_version=1,
|
||||
slug="test-agent",
|
||||
name="Test Agent",
|
||||
sub_heading="Test subheading",
|
||||
video_url="video.mp4",
|
||||
image_urls=["image1.jpg", "image2.jpg"],
|
||||
description="Test description",
|
||||
categories=["cat1", "cat2"],
|
||||
)
|
||||
assert request.agent_id == "agent123"
|
||||
assert request.agent_version == 1
|
||||
assert len(request.image_urls) == 2
|
||||
assert len(request.categories) == 2
|
||||
439
autogpt_platform/backend/backend/server/v2/store/routes.py
Normal file
439
autogpt_platform/backend/backend/server/v2/store/routes.py
Normal file
@@ -0,0 +1,439 @@
|
||||
import logging
|
||||
import typing
|
||||
|
||||
import autogpt_libs.auth.depends
|
||||
import autogpt_libs.auth.middleware
|
||||
import fastapi
|
||||
import fastapi.responses
|
||||
|
||||
import backend.server.v2.store.db
|
||||
import backend.server.v2.store.media
|
||||
import backend.server.v2.store.model
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = fastapi.APIRouter()
|
||||
|
||||
|
||||
##############################################
|
||||
############### Profile Endpoints ############
|
||||
##############################################
|
||||
|
||||
|
||||
@router.get("/profile", tags=["store", "private"])
|
||||
async def get_profile(
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
]
|
||||
) -> backend.server.v2.store.model.ProfileDetails:
|
||||
"""
|
||||
Get the profile details for the authenticated user.
|
||||
"""
|
||||
try:
|
||||
profile = await backend.server.v2.store.db.get_user_profile(user_id)
|
||||
return profile
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting user profile")
|
||||
raise
|
||||
|
||||
|
||||
@router.post(
|
||||
"/profile",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
|
||||
)
|
||||
async def update_or_create_profile(
|
||||
profile: backend.server.v2.store.model.Profile,
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
],
|
||||
) -> backend.server.v2.store.model.CreatorDetails:
|
||||
"""
|
||||
Update the store profile for the authenticated user.
|
||||
|
||||
Args:
|
||||
profile (Profile): The updated profile details
|
||||
user_id (str): ID of the authenticated user
|
||||
|
||||
Returns:
|
||||
CreatorDetails: The updated profile
|
||||
|
||||
Raises:
|
||||
HTTPException: If there is an error updating the profile
|
||||
"""
|
||||
try:
|
||||
updated_profile = await backend.server.v2.store.db.update_or_create_profile(
|
||||
user_id=user_id, profile=profile
|
||||
)
|
||||
return updated_profile
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst updating profile")
|
||||
raise
|
||||
|
||||
|
||||
##############################################
|
||||
############### Agent Endpoints ##############
|
||||
##############################################
|
||||
|
||||
|
||||
@router.get("/agents", tags=["store", "public"])
|
||||
async def get_agents(
|
||||
featured: bool = False,
|
||||
creator: str | None = None,
|
||||
sorted_by: str | None = None,
|
||||
search_query: str | None = None,
|
||||
category: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> backend.server.v2.store.model.StoreAgentsResponse:
|
||||
"""
|
||||
Get a paginated list of agents from the store with optional filtering and sorting.
|
||||
|
||||
Args:
|
||||
featured (bool, optional): Filter to only show featured agents. Defaults to False.
|
||||
creator (str | None, optional): Filter agents by creator username. Defaults to None.
|
||||
sorted_by (str | None, optional): Sort agents by "runs" or "rating". Defaults to None.
|
||||
search_query (str | None, optional): Search agents by name, subheading and description. Defaults to None.
|
||||
category (str | None, optional): Filter agents by category. Defaults to None.
|
||||
page (int, optional): Page number for pagination. Defaults to 1.
|
||||
page_size (int, optional): Number of agents per page. Defaults to 20.
|
||||
|
||||
Returns:
|
||||
StoreAgentsResponse: Paginated list of agents matching the filters
|
||||
|
||||
Raises:
|
||||
HTTPException: If page or page_size are less than 1
|
||||
|
||||
Used for:
|
||||
- Home Page Featured Agents
|
||||
- Home Page Top Agents
|
||||
- Search Results
|
||||
- Agent Details - Other Agents By Creator
|
||||
- Agent Details - Similar Agents
|
||||
- Creator Details - Agents By Creator
|
||||
"""
|
||||
if page < 1:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=422, detail="Page must be greater than 0"
|
||||
)
|
||||
|
||||
if page_size < 1:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=422, detail="Page size must be greater than 0"
|
||||
)
|
||||
|
||||
try:
|
||||
agents = await backend.server.v2.store.db.get_store_agents(
|
||||
featured=featured,
|
||||
creator=creator,
|
||||
sorted_by=sorted_by,
|
||||
search_query=search_query,
|
||||
category=category,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
return agents
|
||||
except Exception:
|
||||
logger.exception("Exception occured whilst getting store agents")
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/agents/{username}/{agent_name}", tags=["store", "public"])
|
||||
async def get_agent(
|
||||
username: str, agent_name: str
|
||||
) -> backend.server.v2.store.model.StoreAgentDetails:
|
||||
"""
|
||||
This is only used on the AgentDetails Page
|
||||
|
||||
It returns the store listing agents details.
|
||||
"""
|
||||
try:
|
||||
agent = await backend.server.v2.store.db.get_store_agent_details(
|
||||
username=username, agent_name=agent_name
|
||||
)
|
||||
return agent
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting store agent details")
|
||||
raise
|
||||
|
||||
|
||||
@router.post(
|
||||
"/agents/{username}/{agent_name}/review",
|
||||
tags=["store"],
|
||||
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
|
||||
)
|
||||
async def create_review(
|
||||
username: str,
|
||||
agent_name: str,
|
||||
review: backend.server.v2.store.model.StoreReviewCreate,
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
],
|
||||
) -> backend.server.v2.store.model.StoreReview:
|
||||
"""
|
||||
Create a review for a store agent.
|
||||
|
||||
Args:
|
||||
username: Creator's username
|
||||
agent_name: Name/slug of the agent
|
||||
review: Review details including score and optional comments
|
||||
user_id: ID of authenticated user creating the review
|
||||
|
||||
Returns:
|
||||
The created review
|
||||
"""
|
||||
try:
|
||||
# Create the review
|
||||
created_review = await backend.server.v2.store.db.create_store_review(
|
||||
user_id=user_id,
|
||||
store_listing_version_id=review.store_listing_version_id,
|
||||
score=review.score,
|
||||
comments=review.comments,
|
||||
)
|
||||
|
||||
return created_review
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst creating store review")
|
||||
raise
|
||||
|
||||
|
||||
##############################################
|
||||
############# Creator Endpoints #############
|
||||
##############################################
|
||||
|
||||
|
||||
@router.get("/creators", tags=["store", "public"])
|
||||
async def get_creators(
|
||||
featured: bool = False,
|
||||
search_query: str | None = None,
|
||||
sorted_by: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> backend.server.v2.store.model.CreatorsResponse:
|
||||
"""
|
||||
This is needed for:
|
||||
- Home Page Featured Creators
|
||||
- Search Results Page
|
||||
|
||||
---
|
||||
|
||||
To support this functionality we need:
|
||||
- featured: bool - to limit the list to just featured agents
|
||||
- search_query: str - vector search based on the creators profile description.
|
||||
- sorted_by: [agent_rating, agent_runs] -
|
||||
"""
|
||||
if page < 1:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=422, detail="Page must be greater than 0"
|
||||
)
|
||||
|
||||
if page_size < 1:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=422, detail="Page size must be greater than 0"
|
||||
)
|
||||
|
||||
try:
|
||||
creators = await backend.server.v2.store.db.get_store_creators(
|
||||
featured=featured,
|
||||
search_query=search_query,
|
||||
sorted_by=sorted_by,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
return creators
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting store creators")
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/creator/{username}", tags=["store", "public"])
|
||||
async def get_creator(username: str) -> backend.server.v2.store.model.CreatorDetails:
|
||||
"""
|
||||
Get the details of a creator
|
||||
- Creator Details Page
|
||||
"""
|
||||
try:
|
||||
creator = await backend.server.v2.store.db.get_store_creator_details(
|
||||
username=username
|
||||
)
|
||||
return creator
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting creator details")
|
||||
raise
|
||||
|
||||
|
||||
############################################
|
||||
############# Store Submissions ###############
|
||||
############################################
|
||||
@router.get(
|
||||
"/myagents",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
|
||||
)
|
||||
async def get_my_agents(
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
]
|
||||
) -> backend.server.v2.store.model.MyAgentsResponse:
|
||||
try:
|
||||
agents = await backend.server.v2.store.db.get_my_agents(user_id)
|
||||
return agents
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting my agents")
|
||||
raise
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/submissions/{submission_id}",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
|
||||
)
|
||||
async def delete_submission(
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
],
|
||||
submission_id: str,
|
||||
) -> bool:
|
||||
"""
|
||||
Delete a store listing submission.
|
||||
|
||||
Args:
|
||||
user_id (str): ID of the authenticated user
|
||||
submission_id (str): ID of the submission to be deleted
|
||||
|
||||
Returns:
|
||||
bool: True if the submission was successfully deleted, False otherwise
|
||||
"""
|
||||
try:
|
||||
result = await backend.server.v2.store.db.delete_store_submission(
|
||||
user_id=user_id,
|
||||
submission_id=submission_id,
|
||||
)
|
||||
return result
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst deleting store submission")
|
||||
raise
|
||||
|
||||
|
||||
@router.get(
|
||||
"/submissions",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
|
||||
)
|
||||
async def get_submissions(
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
],
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> backend.server.v2.store.model.StoreSubmissionsResponse:
|
||||
"""
|
||||
Get a paginated list of store submissions for the authenticated user.
|
||||
|
||||
Args:
|
||||
user_id (str): ID of the authenticated user
|
||||
page (int, optional): Page number for pagination. Defaults to 1.
|
||||
page_size (int, optional): Number of submissions per page. Defaults to 20.
|
||||
|
||||
Returns:
|
||||
StoreListingsResponse: Paginated list of store submissions
|
||||
|
||||
Raises:
|
||||
HTTPException: If page or page_size are less than 1
|
||||
"""
|
||||
if page < 1:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=422, detail="Page must be greater than 0"
|
||||
)
|
||||
|
||||
if page_size < 1:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=422, detail="Page size must be greater than 0"
|
||||
)
|
||||
try:
|
||||
listings = await backend.server.v2.store.db.get_store_submissions(
|
||||
user_id=user_id,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
return listings
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting store submissions")
|
||||
raise
|
||||
|
||||
|
||||
@router.post(
|
||||
"/submissions",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
|
||||
)
|
||||
async def create_submission(
|
||||
submission_request: backend.server.v2.store.model.StoreSubmissionRequest,
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
],
|
||||
) -> backend.server.v2.store.model.StoreSubmission:
|
||||
"""
|
||||
Create a new store listing submission.
|
||||
|
||||
Args:
|
||||
submission_request (StoreSubmissionRequest): The submission details
|
||||
user_id (str): ID of the authenticated user submitting the listing
|
||||
|
||||
Returns:
|
||||
StoreSubmission: The created store submission
|
||||
|
||||
Raises:
|
||||
HTTPException: If there is an error creating the submission
|
||||
"""
|
||||
try:
|
||||
submission = await backend.server.v2.store.db.create_store_submission(
|
||||
user_id=user_id,
|
||||
agent_id=submission_request.agent_id,
|
||||
agent_version=submission_request.agent_version,
|
||||
slug=submission_request.slug,
|
||||
name=submission_request.name,
|
||||
video_url=submission_request.video_url,
|
||||
image_urls=submission_request.image_urls,
|
||||
description=submission_request.description,
|
||||
sub_heading=submission_request.sub_heading,
|
||||
categories=submission_request.categories,
|
||||
)
|
||||
return submission
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst creating store submission")
|
||||
raise
|
||||
|
||||
|
||||
@router.post(
|
||||
"/submissions/media",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
|
||||
)
|
||||
async def upload_submission_media(
|
||||
file: fastapi.UploadFile,
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
],
|
||||
) -> str:
|
||||
"""
|
||||
Upload media (images/videos) for a store listing submission.
|
||||
|
||||
Args:
|
||||
file (UploadFile): The media file to upload
|
||||
user_id (str): ID of the authenticated user uploading the media
|
||||
|
||||
Returns:
|
||||
str: URL of the uploaded media file
|
||||
|
||||
Raises:
|
||||
HTTPException: If there is an error uploading the media
|
||||
"""
|
||||
try:
|
||||
media_url = await backend.server.v2.store.media.upload_media(
|
||||
user_id=user_id, file=file
|
||||
)
|
||||
return media_url
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst uploading submission media")
|
||||
raise
|
||||
551
autogpt_platform/backend/backend/server/v2/store/routes_test.py
Normal file
551
autogpt_platform/backend/backend/server/v2/store/routes_test.py
Normal file
@@ -0,0 +1,551 @@
|
||||
import datetime
|
||||
|
||||
import autogpt_libs.auth.depends
|
||||
import autogpt_libs.auth.middleware
|
||||
import fastapi
|
||||
import fastapi.testclient
|
||||
import prisma.enums
|
||||
import pytest_mock
|
||||
|
||||
import backend.server.v2.store.model
|
||||
import backend.server.v2.store.routes
|
||||
|
||||
app = fastapi.FastAPI()
|
||||
app.include_router(backend.server.v2.store.routes.router)
|
||||
|
||||
client = fastapi.testclient.TestClient(app)
|
||||
|
||||
|
||||
def override_auth_middleware():
|
||||
"""Override auth middleware for testing"""
|
||||
return {"sub": "test-user-id"}
|
||||
|
||||
|
||||
def override_get_user_id():
|
||||
"""Override get_user_id for testing"""
|
||||
return "test-user-id"
|
||||
|
||||
|
||||
app.dependency_overrides[autogpt_libs.auth.middleware.auth_middleware] = (
|
||||
override_auth_middleware
|
||||
)
|
||||
app.dependency_overrides[autogpt_libs.auth.depends.get_user_id] = override_get_user_id
|
||||
|
||||
|
||||
def test_get_agents_defaults(mocker: pytest_mock.MockFixture):
|
||||
mocked_value = backend.server.v2.store.model.StoreAgentsResponse(
|
||||
agents=[],
|
||||
pagination=backend.server.v2.store.model.Pagination(
|
||||
current_page=0,
|
||||
total_items=0,
|
||||
total_pages=0,
|
||||
page_size=10,
|
||||
),
|
||||
)
|
||||
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agents")
|
||||
mock_db_call.return_value = mocked_value
|
||||
response = client.get("/agents")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = backend.server.v2.store.model.StoreAgentsResponse.model_validate(
|
||||
response.json()
|
||||
)
|
||||
assert data.pagination.total_pages == 0
|
||||
assert data.agents == []
|
||||
mock_db_call.assert_called_once_with(
|
||||
featured=False,
|
||||
creator=None,
|
||||
sorted_by=None,
|
||||
search_query=None,
|
||||
category=None,
|
||||
page=1,
|
||||
page_size=20,
|
||||
)
|
||||
|
||||
|
||||
def test_get_agents_featured(mocker: pytest_mock.MockFixture):
|
||||
mocked_value = backend.server.v2.store.model.StoreAgentsResponse(
|
||||
agents=[
|
||||
backend.server.v2.store.model.StoreAgent(
|
||||
slug="featured-agent",
|
||||
agent_name="Featured Agent",
|
||||
agent_image="featured.jpg",
|
||||
creator="creator1",
|
||||
creator_avatar="avatar1.jpg",
|
||||
sub_heading="Featured agent subheading",
|
||||
description="Featured agent description",
|
||||
runs=100,
|
||||
rating=4.5,
|
||||
)
|
||||
],
|
||||
pagination=backend.server.v2.store.model.Pagination(
|
||||
current_page=1,
|
||||
total_items=1,
|
||||
total_pages=1,
|
||||
page_size=20,
|
||||
),
|
||||
)
|
||||
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agents")
|
||||
mock_db_call.return_value = mocked_value
|
||||
response = client.get("/agents?featured=true")
|
||||
assert response.status_code == 200
|
||||
data = backend.server.v2.store.model.StoreAgentsResponse.model_validate(
|
||||
response.json()
|
||||
)
|
||||
assert len(data.agents) == 1
|
||||
assert data.agents[0].slug == "featured-agent"
|
||||
mock_db_call.assert_called_once_with(
|
||||
featured=True,
|
||||
creator=None,
|
||||
sorted_by=None,
|
||||
search_query=None,
|
||||
category=None,
|
||||
page=1,
|
||||
page_size=20,
|
||||
)
|
||||
|
||||
|
||||
def test_get_agents_by_creator(mocker: pytest_mock.MockFixture):
|
||||
mocked_value = backend.server.v2.store.model.StoreAgentsResponse(
|
||||
agents=[
|
||||
backend.server.v2.store.model.StoreAgent(
|
||||
slug="creator-agent",
|
||||
agent_name="Creator Agent",
|
||||
agent_image="agent.jpg",
|
||||
creator="specific-creator",
|
||||
creator_avatar="avatar.jpg",
|
||||
sub_heading="Creator agent subheading",
|
||||
description="Creator agent description",
|
||||
runs=50,
|
||||
rating=4.0,
|
||||
)
|
||||
],
|
||||
pagination=backend.server.v2.store.model.Pagination(
|
||||
current_page=1,
|
||||
total_items=1,
|
||||
total_pages=1,
|
||||
page_size=20,
|
||||
),
|
||||
)
|
||||
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agents")
|
||||
mock_db_call.return_value = mocked_value
|
||||
response = client.get("/agents?creator=specific-creator")
|
||||
assert response.status_code == 200
|
||||
data = backend.server.v2.store.model.StoreAgentsResponse.model_validate(
|
||||
response.json()
|
||||
)
|
||||
assert len(data.agents) == 1
|
||||
assert data.agents[0].creator == "specific-creator"
|
||||
mock_db_call.assert_called_once_with(
|
||||
featured=False,
|
||||
creator="specific-creator",
|
||||
sorted_by=None,
|
||||
search_query=None,
|
||||
category=None,
|
||||
page=1,
|
||||
page_size=20,
|
||||
)
|
||||
|
||||
|
||||
def test_get_agents_sorted(mocker: pytest_mock.MockFixture):
|
||||
mocked_value = backend.server.v2.store.model.StoreAgentsResponse(
|
||||
agents=[
|
||||
backend.server.v2.store.model.StoreAgent(
|
||||
slug="top-agent",
|
||||
agent_name="Top Agent",
|
||||
agent_image="top.jpg",
|
||||
creator="creator1",
|
||||
creator_avatar="avatar1.jpg",
|
||||
sub_heading="Top agent subheading",
|
||||
description="Top agent description",
|
||||
runs=1000,
|
||||
rating=5.0,
|
||||
)
|
||||
],
|
||||
pagination=backend.server.v2.store.model.Pagination(
|
||||
current_page=1,
|
||||
total_items=1,
|
||||
total_pages=1,
|
||||
page_size=20,
|
||||
),
|
||||
)
|
||||
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agents")
|
||||
mock_db_call.return_value = mocked_value
|
||||
response = client.get("/agents?sorted_by=runs")
|
||||
assert response.status_code == 200
|
||||
data = backend.server.v2.store.model.StoreAgentsResponse.model_validate(
|
||||
response.json()
|
||||
)
|
||||
assert len(data.agents) == 1
|
||||
assert data.agents[0].runs == 1000
|
||||
mock_db_call.assert_called_once_with(
|
||||
featured=False,
|
||||
creator=None,
|
||||
sorted_by="runs",
|
||||
search_query=None,
|
||||
category=None,
|
||||
page=1,
|
||||
page_size=20,
|
||||
)
|
||||
|
||||
|
||||
def test_get_agents_search(mocker: pytest_mock.MockFixture):
|
||||
mocked_value = backend.server.v2.store.model.StoreAgentsResponse(
|
||||
agents=[
|
||||
backend.server.v2.store.model.StoreAgent(
|
||||
slug="search-agent",
|
||||
agent_name="Search Agent",
|
||||
agent_image="search.jpg",
|
||||
creator="creator1",
|
||||
creator_avatar="avatar1.jpg",
|
||||
sub_heading="Search agent subheading",
|
||||
description="Specific search term description",
|
||||
runs=75,
|
||||
rating=4.2,
|
||||
)
|
||||
],
|
||||
pagination=backend.server.v2.store.model.Pagination(
|
||||
current_page=1,
|
||||
total_items=1,
|
||||
total_pages=1,
|
||||
page_size=20,
|
||||
),
|
||||
)
|
||||
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agents")
|
||||
mock_db_call.return_value = mocked_value
|
||||
response = client.get("/agents?search_query=specific")
|
||||
assert response.status_code == 200
|
||||
data = backend.server.v2.store.model.StoreAgentsResponse.model_validate(
|
||||
response.json()
|
||||
)
|
||||
assert len(data.agents) == 1
|
||||
assert "specific" in data.agents[0].description.lower()
|
||||
mock_db_call.assert_called_once_with(
|
||||
featured=False,
|
||||
creator=None,
|
||||
sorted_by=None,
|
||||
search_query="specific",
|
||||
category=None,
|
||||
page=1,
|
||||
page_size=20,
|
||||
)
|
||||
|
||||
|
||||
def test_get_agents_category(mocker: pytest_mock.MockFixture):
|
||||
mocked_value = backend.server.v2.store.model.StoreAgentsResponse(
|
||||
agents=[
|
||||
backend.server.v2.store.model.StoreAgent(
|
||||
slug="category-agent",
|
||||
agent_name="Category Agent",
|
||||
agent_image="category.jpg",
|
||||
creator="creator1",
|
||||
creator_avatar="avatar1.jpg",
|
||||
sub_heading="Category agent subheading",
|
||||
description="Category agent description",
|
||||
runs=60,
|
||||
rating=4.1,
|
||||
)
|
||||
],
|
||||
pagination=backend.server.v2.store.model.Pagination(
|
||||
current_page=1,
|
||||
total_items=1,
|
||||
total_pages=1,
|
||||
page_size=20,
|
||||
),
|
||||
)
|
||||
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agents")
|
||||
mock_db_call.return_value = mocked_value
|
||||
response = client.get("/agents?category=test-category")
|
||||
assert response.status_code == 200
|
||||
data = backend.server.v2.store.model.StoreAgentsResponse.model_validate(
|
||||
response.json()
|
||||
)
|
||||
assert len(data.agents) == 1
|
||||
mock_db_call.assert_called_once_with(
|
||||
featured=False,
|
||||
creator=None,
|
||||
sorted_by=None,
|
||||
search_query=None,
|
||||
category="test-category",
|
||||
page=1,
|
||||
page_size=20,
|
||||
)
|
||||
|
||||
|
||||
def test_get_agents_pagination(mocker: pytest_mock.MockFixture):
|
||||
mocked_value = backend.server.v2.store.model.StoreAgentsResponse(
|
||||
agents=[
|
||||
backend.server.v2.store.model.StoreAgent(
|
||||
slug=f"agent-{i}",
|
||||
agent_name=f"Agent {i}",
|
||||
agent_image=f"agent{i}.jpg",
|
||||
creator="creator1",
|
||||
creator_avatar="avatar1.jpg",
|
||||
sub_heading=f"Agent {i} subheading",
|
||||
description=f"Agent {i} description",
|
||||
runs=i * 10,
|
||||
rating=4.0,
|
||||
)
|
||||
for i in range(5)
|
||||
],
|
||||
pagination=backend.server.v2.store.model.Pagination(
|
||||
current_page=2,
|
||||
total_items=15,
|
||||
total_pages=3,
|
||||
page_size=5,
|
||||
),
|
||||
)
|
||||
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agents")
|
||||
mock_db_call.return_value = mocked_value
|
||||
response = client.get("/agents?page=2&page_size=5")
|
||||
assert response.status_code == 200
|
||||
data = backend.server.v2.store.model.StoreAgentsResponse.model_validate(
|
||||
response.json()
|
||||
)
|
||||
assert len(data.agents) == 5
|
||||
assert data.pagination.current_page == 2
|
||||
assert data.pagination.page_size == 5
|
||||
mock_db_call.assert_called_once_with(
|
||||
featured=False,
|
||||
creator=None,
|
||||
sorted_by=None,
|
||||
search_query=None,
|
||||
category=None,
|
||||
page=2,
|
||||
page_size=5,
|
||||
)
|
||||
|
||||
|
||||
def test_get_agents_malformed_request(mocker: pytest_mock.MockFixture):
|
||||
# Test with invalid page number
|
||||
response = client.get("/agents?page=-1")
|
||||
assert response.status_code == 422
|
||||
|
||||
# Test with invalid page size
|
||||
response = client.get("/agents?page_size=0")
|
||||
assert response.status_code == 422
|
||||
|
||||
# Test with non-numeric values
|
||||
response = client.get("/agents?page=abc&page_size=def")
|
||||
assert response.status_code == 422
|
||||
|
||||
# Verify no DB calls were made
|
||||
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agents")
|
||||
mock_db_call.assert_not_called()
|
||||
|
||||
|
||||
def test_get_agent_details(mocker: pytest_mock.MockFixture):
|
||||
mocked_value = backend.server.v2.store.model.StoreAgentDetails(
|
||||
store_listing_version_id="test-version-id",
|
||||
slug="test-agent",
|
||||
agent_name="Test Agent",
|
||||
agent_video="video.mp4",
|
||||
agent_image=["image1.jpg", "image2.jpg"],
|
||||
creator="creator1",
|
||||
creator_avatar="avatar1.jpg",
|
||||
sub_heading="Test agent subheading",
|
||||
description="Test agent description",
|
||||
categories=["category1", "category2"],
|
||||
runs=100,
|
||||
rating=4.5,
|
||||
versions=["1.0.0", "1.1.0"],
|
||||
last_updated=datetime.datetime.now(),
|
||||
)
|
||||
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agent_details")
|
||||
mock_db_call.return_value = mocked_value
|
||||
|
||||
response = client.get("/agents/creator1/test-agent")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = backend.server.v2.store.model.StoreAgentDetails.model_validate(
|
||||
response.json()
|
||||
)
|
||||
assert data.agent_name == "Test Agent"
|
||||
assert data.creator == "creator1"
|
||||
mock_db_call.assert_called_once_with(username="creator1", agent_name="test-agent")
|
||||
|
||||
|
||||
def test_get_creators_defaults(mocker: pytest_mock.MockFixture):
|
||||
mocked_value = backend.server.v2.store.model.CreatorsResponse(
|
||||
creators=[],
|
||||
pagination=backend.server.v2.store.model.Pagination(
|
||||
current_page=0,
|
||||
total_items=0,
|
||||
total_pages=0,
|
||||
page_size=10,
|
||||
),
|
||||
)
|
||||
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_creators")
|
||||
mock_db_call.return_value = mocked_value
|
||||
|
||||
response = client.get("/creators")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = backend.server.v2.store.model.CreatorsResponse.model_validate(
|
||||
response.json()
|
||||
)
|
||||
assert data.pagination.total_pages == 0
|
||||
assert data.creators == []
|
||||
mock_db_call.assert_called_once_with(
|
||||
featured=False, search_query=None, sorted_by=None, page=1, page_size=20
|
||||
)
|
||||
|
||||
|
||||
def test_get_creators_pagination(mocker: pytest_mock.MockFixture):
|
||||
mocked_value = backend.server.v2.store.model.CreatorsResponse(
|
||||
creators=[
|
||||
backend.server.v2.store.model.Creator(
|
||||
name=f"Creator {i}",
|
||||
username=f"creator{i}",
|
||||
description=f"Creator {i} description",
|
||||
avatar_url=f"avatar{i}.jpg",
|
||||
num_agents=1,
|
||||
agent_rating=4.5,
|
||||
agent_runs=100,
|
||||
)
|
||||
for i in range(5)
|
||||
],
|
||||
pagination=backend.server.v2.store.model.Pagination(
|
||||
current_page=2,
|
||||
total_items=15,
|
||||
total_pages=3,
|
||||
page_size=5,
|
||||
),
|
||||
)
|
||||
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_creators")
|
||||
mock_db_call.return_value = mocked_value
|
||||
|
||||
response = client.get("/creators?page=2&page_size=5")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = backend.server.v2.store.model.CreatorsResponse.model_validate(
|
||||
response.json()
|
||||
)
|
||||
assert len(data.creators) == 5
|
||||
assert data.pagination.current_page == 2
|
||||
assert data.pagination.page_size == 5
|
||||
mock_db_call.assert_called_once_with(
|
||||
featured=False, search_query=None, sorted_by=None, page=2, page_size=5
|
||||
)
|
||||
|
||||
|
||||
def test_get_creators_malformed_request(mocker: pytest_mock.MockFixture):
|
||||
# Test with invalid page number
|
||||
response = client.get("/creators?page=-1")
|
||||
assert response.status_code == 422
|
||||
|
||||
# Test with invalid page size
|
||||
response = client.get("/creators?page_size=0")
|
||||
assert response.status_code == 422
|
||||
|
||||
# Test with non-numeric values
|
||||
response = client.get("/creators?page=abc&page_size=def")
|
||||
assert response.status_code == 422
|
||||
|
||||
# Verify no DB calls were made
|
||||
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_creators")
|
||||
mock_db_call.assert_not_called()
|
||||
|
||||
|
||||
def test_get_creator_details(mocker: pytest_mock.MockFixture):
|
||||
mocked_value = backend.server.v2.store.model.CreatorDetails(
|
||||
name="Test User",
|
||||
username="creator1",
|
||||
description="Test creator description",
|
||||
links=["link1.com", "link2.com"],
|
||||
avatar_url="avatar.jpg",
|
||||
agent_rating=4.8,
|
||||
agent_runs=1000,
|
||||
top_categories=["category1", "category2"],
|
||||
)
|
||||
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_creator_details")
|
||||
mock_db_call.return_value = mocked_value
|
||||
|
||||
response = client.get("/creator/creator1")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = backend.server.v2.store.model.CreatorDetails.model_validate(response.json())
|
||||
assert data.username == "creator1"
|
||||
assert data.name == "Test User"
|
||||
mock_db_call.assert_called_once_with(username="creator1")
|
||||
|
||||
|
||||
def test_get_submissions_success(mocker: pytest_mock.MockFixture):
|
||||
mocked_value = backend.server.v2.store.model.StoreSubmissionsResponse(
|
||||
submissions=[
|
||||
backend.server.v2.store.model.StoreSubmission(
|
||||
name="Test Agent",
|
||||
description="Test agent description",
|
||||
image_urls=["test.jpg"],
|
||||
date_submitted=datetime.datetime.now(),
|
||||
status=prisma.enums.SubmissionStatus.APPROVED,
|
||||
runs=50,
|
||||
rating=4.2,
|
||||
agent_id="test-agent-id",
|
||||
agent_version=1,
|
||||
sub_heading="Test agent subheading",
|
||||
slug="test-agent",
|
||||
)
|
||||
],
|
||||
pagination=backend.server.v2.store.model.Pagination(
|
||||
current_page=1,
|
||||
total_items=1,
|
||||
total_pages=1,
|
||||
page_size=20,
|
||||
),
|
||||
)
|
||||
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_submissions")
|
||||
mock_db_call.return_value = mocked_value
|
||||
|
||||
response = client.get("/submissions")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = backend.server.v2.store.model.StoreSubmissionsResponse.model_validate(
|
||||
response.json()
|
||||
)
|
||||
assert len(data.submissions) == 1
|
||||
assert data.submissions[0].name == "Test Agent"
|
||||
assert data.pagination.current_page == 1
|
||||
mock_db_call.assert_called_once_with(user_id="test-user-id", page=1, page_size=20)
|
||||
|
||||
|
||||
def test_get_submissions_pagination(mocker: pytest_mock.MockFixture):
|
||||
mocked_value = backend.server.v2.store.model.StoreSubmissionsResponse(
|
||||
submissions=[],
|
||||
pagination=backend.server.v2.store.model.Pagination(
|
||||
current_page=2,
|
||||
total_items=10,
|
||||
total_pages=2,
|
||||
page_size=5,
|
||||
),
|
||||
)
|
||||
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_submissions")
|
||||
mock_db_call.return_value = mocked_value
|
||||
|
||||
response = client.get("/submissions?page=2&page_size=5")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = backend.server.v2.store.model.StoreSubmissionsResponse.model_validate(
|
||||
response.json()
|
||||
)
|
||||
assert data.pagination.current_page == 2
|
||||
assert data.pagination.page_size == 5
|
||||
mock_db_call.assert_called_once_with(user_id="test-user-id", page=2, page_size=5)
|
||||
|
||||
|
||||
def test_get_submissions_malformed_request(mocker: pytest_mock.MockFixture):
|
||||
# Test with invalid page number
|
||||
response = client.get("/submissions?page=-1")
|
||||
assert response.status_code == 422
|
||||
|
||||
# Test with invalid page size
|
||||
response = client.get("/submissions?page_size=0")
|
||||
assert response.status_code == 422
|
||||
|
||||
# Test with non-numeric values
|
||||
response = client.get("/submissions?page=abc&page_size=def")
|
||||
assert response.status_code == 422
|
||||
|
||||
# Verify no DB calls were made
|
||||
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_submissions")
|
||||
mock_db_call.assert_not_called()
|
||||
@@ -148,6 +148,16 @@ class Config(UpdateTrackingModel["Config"], BaseSettings):
|
||||
"This value is then used to generate redirect URLs for OAuth flows.",
|
||||
)
|
||||
|
||||
media_gcs_bucket_name: str = Field(
|
||||
default="",
|
||||
description="The name of the Google Cloud Storage bucket for media files",
|
||||
)
|
||||
|
||||
google_application_credentials: str = Field(
|
||||
default="",
|
||||
description="The path to the Google Cloud credentials JSON file",
|
||||
)
|
||||
|
||||
@field_validator("platform_base_url", "frontend_base_url")
|
||||
@classmethod
|
||||
def validate_platform_base_url(cls, v: str, info: ValidationInfo) -> str:
|
||||
|
||||
@@ -1,20 +1,31 @@
|
||||
version: "3"
|
||||
services:
|
||||
postgres-test:
|
||||
image: ankane/pgvector:latest
|
||||
environment:
|
||||
- POSTGRES_USER=agpt_user
|
||||
- POSTGRES_PASSWORD=pass123
|
||||
- POSTGRES_DB=agpt_local
|
||||
- POSTGRES_USER=${DB_USER}
|
||||
- POSTGRES_PASSWORD=${DB_PASS}
|
||||
- POSTGRES_DB=${DB_NAME}
|
||||
healthcheck:
|
||||
test: pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
ports:
|
||||
- "5433:5432"
|
||||
- "${DB_PORT}:5432"
|
||||
networks:
|
||||
- app-network-test
|
||||
redis-test:
|
||||
image: redis:latest
|
||||
command: redis-server --requirepass password
|
||||
ports:
|
||||
- "6379:6379"
|
||||
networks:
|
||||
- app-network-test
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
networks:
|
||||
app-network-test:
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SubmissionStatus" AS ENUM ('DAFT', 'PENDING', 'APPROVED', 'REJECTED');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "AgentGraphExecution" ADD COLUMN "agentPresetId" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "AgentNodeExecutionInputOutput" ADD COLUMN "agentPresetId" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "AnalyticsMetrics" ALTER COLUMN "id" DROP DEFAULT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "CreditTransaction" RENAME CONSTRAINT "UserBlockCredit_pkey" TO "CreditTransaction_pkey";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AgentPreset" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"userId" TEXT NOT NULL,
|
||||
"agentId" TEXT NOT NULL,
|
||||
"agentVersion" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "AgentPreset_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserAgent" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"userId" TEXT NOT NULL,
|
||||
"agentId" TEXT NOT NULL,
|
||||
"agentVersion" INTEGER NOT NULL,
|
||||
"agentPresetId" TEXT,
|
||||
"isFavorite" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isCreatedByUser" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isArchived" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isDeleted" BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT "UserAgent_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Profile" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"userId" TEXT,
|
||||
"name" TEXT NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"links" TEXT[],
|
||||
"avatarUrl" TEXT,
|
||||
|
||||
CONSTRAINT "Profile_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "StoreListing" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"isDeleted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isApproved" BOOLEAN NOT NULL DEFAULT false,
|
||||
"agentId" TEXT NOT NULL,
|
||||
"agentVersion" INTEGER NOT NULL,
|
||||
"owningUserId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "StoreListing_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "StoreListingVersion" (
|
||||
"id" TEXT NOT NULL,
|
||||
"version" INTEGER NOT NULL DEFAULT 1,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"agentId" TEXT NOT NULL,
|
||||
"agentVersion" INTEGER NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"subHeading" TEXT NOT NULL,
|
||||
"videoUrl" TEXT,
|
||||
"imageUrls" TEXT[],
|
||||
"description" TEXT NOT NULL,
|
||||
"categories" TEXT[],
|
||||
"isFeatured" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isDeleted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isAvailable" BOOLEAN NOT NULL DEFAULT true,
|
||||
"isApproved" BOOLEAN NOT NULL DEFAULT false,
|
||||
"storeListingId" TEXT,
|
||||
|
||||
CONSTRAINT "StoreListingVersion_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "StoreListingReview" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"storeListingVersionId" TEXT NOT NULL,
|
||||
"reviewByUserId" TEXT NOT NULL,
|
||||
"score" INTEGER NOT NULL,
|
||||
"comments" TEXT,
|
||||
|
||||
CONSTRAINT "StoreListingReview_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "StoreListingSubmission" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"storeListingId" TEXT NOT NULL,
|
||||
"storeListingVersionId" TEXT NOT NULL,
|
||||
"reviewerId" TEXT NOT NULL,
|
||||
"Status" "SubmissionStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"reviewComments" TEXT,
|
||||
|
||||
CONSTRAINT "StoreListingSubmission_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AgentPreset_userId_idx" ON "AgentPreset"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "UserAgent_userId_idx" ON "UserAgent"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Profile_username_key" ON "Profile"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Profile_username_idx" ON "Profile"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Profile_userId_idx" ON "Profile"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "StoreListing_isApproved_idx" ON "StoreListing"("isApproved");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "StoreListing_agentId_idx" ON "StoreListing"("agentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "StoreListing_owningUserId_idx" ON "StoreListing"("owningUserId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "StoreListingVersion_agentId_agentVersion_isApproved_idx" ON "StoreListingVersion"("agentId", "agentVersion", "isApproved");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "StoreListingVersion_agentId_agentVersion_key" ON "StoreListingVersion"("agentId", "agentVersion");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "StoreListingReview_storeListingVersionId_idx" ON "StoreListingReview"("storeListingVersionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "StoreListingReview_storeListingVersionId_reviewByUserId_key" ON "StoreListingReview"("storeListingVersionId", "reviewByUserId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "StoreListingSubmission_storeListingId_idx" ON "StoreListingSubmission"("storeListingId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "StoreListingSubmission_Status_idx" ON "StoreListingSubmission"("Status");
|
||||
|
||||
-- RenameForeignKey
|
||||
ALTER TABLE "CreditTransaction" RENAME CONSTRAINT "UserBlockCredit_blockId_fkey" TO "CreditTransaction_blockId_fkey";
|
||||
|
||||
-- RenameForeignKey
|
||||
ALTER TABLE "CreditTransaction" RENAME CONSTRAINT "UserBlockCredit_userId_fkey" TO "CreditTransaction_userId_fkey";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AgentPreset" ADD CONSTRAINT "AgentPreset_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AgentPreset" ADD CONSTRAINT "AgentPreset_agentId_agentVersion_fkey" FOREIGN KEY ("agentId", "agentVersion") REFERENCES "AgentGraph"("id", "version") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserAgent" ADD CONSTRAINT "UserAgent_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserAgent" ADD CONSTRAINT "UserAgent_agentId_agentVersion_fkey" FOREIGN KEY ("agentId", "agentVersion") REFERENCES "AgentGraph"("id", "version") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserAgent" ADD CONSTRAINT "UserAgent_agentPresetId_fkey" FOREIGN KEY ("agentPresetId") REFERENCES "AgentPreset"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AgentGraphExecution" ADD CONSTRAINT "AgentGraphExecution_agentPresetId_fkey" FOREIGN KEY ("agentPresetId") REFERENCES "AgentPreset"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AgentNodeExecutionInputOutput" ADD CONSTRAINT "AgentNodeExecutionInputOutput_agentPresetId_fkey" FOREIGN KEY ("agentPresetId") REFERENCES "AgentPreset"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Profile" ADD CONSTRAINT "Profile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StoreListing" ADD CONSTRAINT "StoreListing_agentId_agentVersion_fkey" FOREIGN KEY ("agentId", "agentVersion") REFERENCES "AgentGraph"("id", "version") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StoreListing" ADD CONSTRAINT "StoreListing_owningUserId_fkey" FOREIGN KEY ("owningUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StoreListingVersion" ADD CONSTRAINT "StoreListingVersion_agentId_agentVersion_fkey" FOREIGN KEY ("agentId", "agentVersion") REFERENCES "AgentGraph"("id", "version") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StoreListingVersion" ADD CONSTRAINT "StoreListingVersion_storeListingId_fkey" FOREIGN KEY ("storeListingId") REFERENCES "StoreListing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StoreListingReview" ADD CONSTRAINT "StoreListingReview_storeListingVersionId_fkey" FOREIGN KEY ("storeListingVersionId") REFERENCES "StoreListingVersion"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StoreListingReview" ADD CONSTRAINT "StoreListingReview_reviewByUserId_fkey" FOREIGN KEY ("reviewByUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StoreListingSubmission" ADD CONSTRAINT "StoreListingSubmission_storeListingId_fkey" FOREIGN KEY ("storeListingId") REFERENCES "StoreListing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StoreListingSubmission" ADD CONSTRAINT "StoreListingSubmission_storeListingVersionId_fkey" FOREIGN KEY ("storeListingVersionId") REFERENCES "StoreListingVersion"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "StoreListingSubmission" ADD CONSTRAINT "StoreListingSubmission_reviewerId_fkey" FOREIGN KEY ("reviewerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "UserBlockCredit_userId_createdAt_idx" RENAME TO "CreditTransaction_userId_createdAt_idx";
|
||||
@@ -0,0 +1,118 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE VIEW "StoreAgent" AS
|
||||
WITH ReviewStats AS (
|
||||
SELECT sl."id" AS "storeListingId",
|
||||
COUNT(sr.id) AS review_count,
|
||||
AVG(CAST(sr.score AS DECIMAL)) AS avg_rating
|
||||
FROM "StoreListing" sl
|
||||
JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl."id"
|
||||
JOIN "StoreListingReview" sr ON sr."storeListingVersionId" = slv.id
|
||||
WHERE sl."isDeleted" = FALSE
|
||||
GROUP BY sl."id"
|
||||
),
|
||||
AgentRuns AS (
|
||||
SELECT "agentGraphId", COUNT(*) AS run_count
|
||||
FROM "AgentGraphExecution"
|
||||
GROUP BY "agentGraphId"
|
||||
)
|
||||
SELECT
|
||||
sl.id AS listing_id,
|
||||
slv.id AS "storeListingVersionId",
|
||||
slv."createdAt" AS updated_at,
|
||||
slv.slug,
|
||||
a.name AS agent_name,
|
||||
slv."videoUrl" AS agent_video,
|
||||
COALESCE(slv."imageUrls", ARRAY[]::TEXT[]) AS agent_image,
|
||||
slv."isFeatured" AS featured,
|
||||
p.username AS creator_username,
|
||||
p."avatarUrl" AS creator_avatar,
|
||||
slv."subHeading" AS sub_heading,
|
||||
slv.description,
|
||||
slv.categories,
|
||||
COALESCE(ar.run_count, 0) AS runs,
|
||||
CAST(COALESCE(rs.avg_rating, 0.0) AS DOUBLE PRECISION) AS rating,
|
||||
ARRAY_AGG(DISTINCT CAST(slv.version AS TEXT)) AS versions
|
||||
FROM "StoreListing" sl
|
||||
JOIN "AgentGraph" a ON sl."agentId" = a.id AND sl."agentVersion" = a."version"
|
||||
LEFT JOIN "Profile" p ON sl."owningUserId" = p."userId"
|
||||
LEFT JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl.id
|
||||
LEFT JOIN ReviewStats rs ON sl.id = rs."storeListingId"
|
||||
LEFT JOIN AgentRuns ar ON a.id = ar."agentGraphId"
|
||||
WHERE sl."isDeleted" = FALSE
|
||||
AND sl."isApproved" = TRUE
|
||||
GROUP BY sl.id, slv.id, slv.slug, slv."createdAt", a.name, slv."videoUrl", slv."imageUrls", slv."isFeatured",
|
||||
p.username, p."avatarUrl", slv."subHeading", slv.description, slv.categories,
|
||||
ar.run_count, rs.avg_rating;
|
||||
|
||||
CREATE VIEW "Creator" AS
|
||||
WITH AgentStats AS (
|
||||
SELECT
|
||||
p.username,
|
||||
COUNT(DISTINCT sl.id) as num_agents,
|
||||
AVG(CAST(COALESCE(sr.score, 0) AS DECIMAL)) as agent_rating,
|
||||
SUM(COALESCE(age.run_count, 0)) as agent_runs
|
||||
FROM "Profile" p
|
||||
LEFT JOIN "StoreListing" sl ON sl."owningUserId" = p."userId"
|
||||
LEFT JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl.id
|
||||
LEFT JOIN "StoreListingReview" sr ON sr."storeListingVersionId" = slv.id
|
||||
LEFT JOIN (
|
||||
SELECT "agentGraphId", COUNT(*) as run_count
|
||||
FROM "AgentGraphExecution"
|
||||
GROUP BY "agentGraphId"
|
||||
) age ON age."agentGraphId" = sl."agentId"
|
||||
WHERE sl."isDeleted" = FALSE AND sl."isApproved" = TRUE
|
||||
GROUP BY p.username
|
||||
)
|
||||
SELECT
|
||||
p.username,
|
||||
p.name,
|
||||
p."avatarUrl" as avatar_url,
|
||||
p.description,
|
||||
ARRAY_AGG(DISTINCT c) FILTER (WHERE c IS NOT NULL) as top_categories,
|
||||
p.links,
|
||||
COALESCE(ast.num_agents, 0) as num_agents,
|
||||
COALESCE(ast.agent_rating, 0.0) as agent_rating,
|
||||
COALESCE(ast.agent_runs, 0) as agent_runs
|
||||
FROM "Profile" p
|
||||
LEFT JOIN AgentStats ast ON ast.username = p.username
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT UNNEST(slv.categories) as c
|
||||
FROM "StoreListing" sl
|
||||
JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl.id
|
||||
WHERE sl."owningUserId" = p."userId"
|
||||
AND sl."isDeleted" = FALSE
|
||||
AND sl."isApproved" = TRUE
|
||||
) cats ON true
|
||||
GROUP BY p.username, p.name, p."avatarUrl", p.description, p.links,
|
||||
ast.num_agents, ast.agent_rating, ast.agent_runs;
|
||||
|
||||
CREATE VIEW "StoreSubmission" AS
|
||||
SELECT
|
||||
sl.id as listing_id,
|
||||
sl."owningUserId" as user_id,
|
||||
slv."agentId" as agent_id,
|
||||
slv."version" as agent_version,
|
||||
slv.slug,
|
||||
slv.name,
|
||||
slv."subHeading" as sub_heading,
|
||||
slv.description,
|
||||
slv."imageUrls" as image_urls,
|
||||
slv."createdAt" as date_submitted,
|
||||
COALESCE(sls."Status", 'PENDING') as status,
|
||||
COALESCE(ar.run_count, 0) as runs,
|
||||
CAST(COALESCE(AVG(CAST(sr.score AS DECIMAL)), 0.0) AS DOUBLE PRECISION) as rating
|
||||
FROM "StoreListing" sl
|
||||
JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl.id
|
||||
LEFT JOIN "StoreListingSubmission" sls ON sls."storeListingId" = sl.id
|
||||
LEFT JOIN "StoreListingReview" sr ON sr."storeListingVersionId" = slv.id
|
||||
LEFT JOIN (
|
||||
SELECT "agentGraphId", COUNT(*) as run_count
|
||||
FROM "AgentGraphExecution"
|
||||
GROUP BY "agentGraphId"
|
||||
) ar ON ar."agentGraphId" = slv."agentId"
|
||||
WHERE sl."isDeleted" = FALSE
|
||||
GROUP BY sl.id, sl."owningUserId", slv."agentId", slv."version", slv.slug, slv.name, slv."subHeading",
|
||||
slv.description, slv."imageUrls", slv."createdAt", sls."Status", ar.run_count;
|
||||
|
||||
COMMIT;
|
||||
2485
autogpt_platform/backend/poetry.lock
generated
2485
autogpt_platform/backend/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ packages = [{ include = "backend" }]
|
||||
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
python = ">=3.10,<3.13"
|
||||
aio-pika = "^9.5.0"
|
||||
anthropic = "^0.39.0"
|
||||
apscheduler = "^3.11.0"
|
||||
@@ -49,8 +49,10 @@ googlemaps = "^4.10.0"
|
||||
replicate = "^1.0.4"
|
||||
pinecone = "^5.3.1"
|
||||
cryptography = "^43.0.3"
|
||||
python-multipart = "^0.0.17"
|
||||
sqlalchemy = "^2.0.36"
|
||||
psycopg2-binary = "^2.9.10"
|
||||
google-cloud-storage = "^2.18.2"
|
||||
launchdarkly-server-sdk = "^9.8.0"
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
poethepoet = "^0.31.0"
|
||||
@@ -62,6 +64,8 @@ pyright = "^1.1.389"
|
||||
isort = "^5.13.2"
|
||||
black = "^24.10.0"
|
||||
aiohappyeyeballs = "^2.4.3"
|
||||
pytest-mock = "^3.14.0"
|
||||
faker = "^30.8.2"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
|
||||
@@ -16,9 +16,9 @@ def wait_for_postgres(max_retries=5, delay=5):
|
||||
"postgres-test",
|
||||
"pg_isready",
|
||||
"-U",
|
||||
"agpt_user",
|
||||
"postgres",
|
||||
"-d",
|
||||
"agpt_local",
|
||||
"postgres",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
|
||||
@@ -8,6 +8,7 @@ generator client {
|
||||
provider = "prisma-client-py"
|
||||
recursive_type_depth = 5
|
||||
interface = "asyncio"
|
||||
previewFeatures = ["views"]
|
||||
}
|
||||
|
||||
// User model to mirror Auth provider users
|
||||
@@ -24,11 +25,19 @@ model User {
|
||||
// Relations
|
||||
AgentGraphs AgentGraph[]
|
||||
AgentGraphExecutions AgentGraphExecution[]
|
||||
IntegrationWebhooks IntegrationWebhook[]
|
||||
AnalyticsDetails AnalyticsDetails[]
|
||||
AnalyticsMetrics AnalyticsMetrics[]
|
||||
CreditTransaction CreditTransaction[]
|
||||
APIKeys APIKey[]
|
||||
|
||||
AgentPreset AgentPreset[]
|
||||
UserAgent UserAgent[]
|
||||
|
||||
Profile Profile[]
|
||||
StoreListing StoreListing[]
|
||||
StoreListingReview StoreListingReview[]
|
||||
StoreListingSubmission StoreListingSubmission[]
|
||||
APIKeys APIKey[]
|
||||
IntegrationWebhooks IntegrationWebhook[]
|
||||
|
||||
@@index([id])
|
||||
@@index([email])
|
||||
@@ -48,15 +57,89 @@ model AgentGraph {
|
||||
|
||||
// Link to User model
|
||||
userId String
|
||||
// FIX: Do not cascade delete the agent when the user is deleted
|
||||
// This allows us to delete user data with deleting the agent which maybe in use by other users
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
AgentNodes AgentNode[]
|
||||
AgentGraphExecution AgentGraphExecution[]
|
||||
|
||||
AgentPreset AgentPreset[]
|
||||
UserAgent UserAgent[]
|
||||
StoreListing StoreListing[]
|
||||
StoreListingVersion StoreListingVersion?
|
||||
|
||||
@@id(name: "graphVersionId", [id, version])
|
||||
@@index([userId, isActive])
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////
|
||||
//////////////// USER SPECIFIC DATA ////////////////////
|
||||
////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
// An AgentPrest is an Agent + User Configuration of that agent.
|
||||
// For example, if someone has created a weather agent and they want to set it up to
|
||||
// Inform them of extreme weather warnings in Texas, the agent with the configuration to set it to
|
||||
// monitor texas, along with the cron setup or webhook tiggers, is an AgentPreset
|
||||
model AgentPreset {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
name String
|
||||
description String
|
||||
|
||||
// For agents that can be triggered by webhooks or cronjob
|
||||
// This bool allows us to disable a configured agent without deleting it
|
||||
isActive Boolean @default(true)
|
||||
|
||||
userId String
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
agentId String
|
||||
agentVersion Int
|
||||
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade)
|
||||
|
||||
InputPresets AgentNodeExecutionInputOutput[] @relation("AgentPresetsInputData")
|
||||
UserAgents UserAgent[]
|
||||
AgentExecution AgentGraphExecution[]
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
// For the library page
|
||||
// It is a user controlled list of agents, that they will see in there library
|
||||
model UserAgent {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
userId String
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
agentId String
|
||||
agentVersion Int
|
||||
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version])
|
||||
|
||||
agentPresetId String?
|
||||
AgentPreset AgentPreset? @relation(fields: [agentPresetId], references: [id])
|
||||
|
||||
isFavorite Boolean @default(false)
|
||||
isCreatedByUser Boolean @default(false)
|
||||
isArchived Boolean @default(false)
|
||||
isDeleted Boolean @default(false)
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////
|
||||
//////// AGENT DEFINITION AND EXECUTION TABLES ////////
|
||||
////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
// This model describes a single node in the Agent Graph/Flow (Multi Agent System).
|
||||
model AgentNode {
|
||||
id String @id @default(uuid())
|
||||
@@ -155,7 +238,9 @@ model AgentGraphExecution {
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
stats String? // JSON serialized object
|
||||
stats String? // JSON serialized object
|
||||
AgentPreset AgentPreset? @relation(fields: [agentPresetId], references: [id])
|
||||
agentPresetId String?
|
||||
|
||||
@@index([agentGraphId, agentGraphVersion])
|
||||
@@index([userId])
|
||||
@@ -202,6 +287,9 @@ model AgentNodeExecutionInputOutput {
|
||||
referencedByOutputExecId String?
|
||||
ReferencedByOutputExec AgentNodeExecution? @relation("AgentNodeExecutionOutput", fields: [referencedByOutputExecId], references: [id], onDelete: Cascade)
|
||||
|
||||
agentPresetId String?
|
||||
AgentPreset AgentPreset? @relation("AgentPresetsInputData", fields: [agentPresetId], references: [id])
|
||||
|
||||
// Input and Output pin names are unique for each AgentNodeExecution.
|
||||
@@unique([referencedByInputExecId, referencedByOutputExecId, name])
|
||||
@@index([referencedByOutputExecId])
|
||||
@@ -256,8 +344,13 @@ model AnalyticsDetails {
|
||||
@@index([type])
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////
|
||||
////////////// METRICS TRACKING TABLES ////////////////
|
||||
////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////
|
||||
model AnalyticsMetrics {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()"))
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -281,6 +374,11 @@ enum CreditTransactionType {
|
||||
USAGE
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////
|
||||
//////// ACCOUNTING AND CREDIT SYSTEM TABLES //////////
|
||||
////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////
|
||||
model CreditTransaction {
|
||||
transactionKey String @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
@@ -301,6 +399,205 @@ model CreditTransaction {
|
||||
@@index([userId, createdAt])
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////
|
||||
////////////// Store TABLES ///////////////////////////
|
||||
////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
model Profile {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
// Only 1 of user or group can be set.
|
||||
// The user this profile belongs to, if any.
|
||||
userId String?
|
||||
User User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
name String
|
||||
username String @unique
|
||||
description String
|
||||
|
||||
links String[]
|
||||
|
||||
avatarUrl String?
|
||||
|
||||
@@index([username])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
view Creator {
|
||||
username String @unique
|
||||
name String
|
||||
avatar_url String
|
||||
description String
|
||||
|
||||
top_categories String[]
|
||||
links String[]
|
||||
|
||||
num_agents Int
|
||||
agent_rating Float
|
||||
agent_runs Int
|
||||
}
|
||||
|
||||
view StoreAgent {
|
||||
listing_id String @id
|
||||
storeListingVersionId String
|
||||
updated_at DateTime
|
||||
|
||||
slug String
|
||||
agent_name String
|
||||
agent_video String?
|
||||
agent_image String[]
|
||||
|
||||
featured Boolean @default(false)
|
||||
creator_username String
|
||||
creator_avatar String
|
||||
sub_heading String
|
||||
description String
|
||||
categories String[]
|
||||
runs Int
|
||||
rating Float
|
||||
versions String[]
|
||||
|
||||
@@unique([creator_username, slug])
|
||||
@@index([creator_username])
|
||||
@@index([featured])
|
||||
@@index([categories])
|
||||
@@index([storeListingVersionId])
|
||||
}
|
||||
|
||||
view StoreSubmission {
|
||||
listing_id String @id
|
||||
user_id String
|
||||
slug String
|
||||
name String
|
||||
sub_heading String
|
||||
description String
|
||||
image_urls String[]
|
||||
date_submitted DateTime
|
||||
status SubmissionStatus
|
||||
runs Int
|
||||
rating Float
|
||||
agent_id String
|
||||
agent_version Int
|
||||
|
||||
@@index([user_id])
|
||||
}
|
||||
|
||||
model StoreListing {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
isDeleted Boolean @default(false)
|
||||
// Not needed but makes lookups faster
|
||||
isApproved Boolean @default(false)
|
||||
|
||||
// The agent link here is only so we can do lookup on agentId, for the listing the StoreListingVersion is used.
|
||||
agentId String
|
||||
agentVersion Int
|
||||
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade)
|
||||
|
||||
owningUserId String
|
||||
OwningUser User @relation(fields: [owningUserId], references: [id])
|
||||
|
||||
StoreListingVersions StoreListingVersion[]
|
||||
StoreListingSubmission StoreListingSubmission[]
|
||||
|
||||
@@index([isApproved])
|
||||
@@index([agentId])
|
||||
@@index([owningUserId])
|
||||
}
|
||||
|
||||
model StoreListingVersion {
|
||||
id String @id @default(uuid())
|
||||
version Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
// The agent and version to be listed on the store
|
||||
agentId String
|
||||
agentVersion Int
|
||||
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version])
|
||||
|
||||
// The detials for this version of the agent, this allows the author to update the details of the agent,
|
||||
// But still allow using old versions of the agent with there original details.
|
||||
// TODO: Create a database view that shows only the latest version of each store listing.
|
||||
slug String
|
||||
name String
|
||||
subHeading String
|
||||
videoUrl String?
|
||||
imageUrls String[]
|
||||
description String
|
||||
categories String[]
|
||||
|
||||
isFeatured Boolean @default(false)
|
||||
|
||||
isDeleted Boolean @default(false)
|
||||
// Old versions can be made unavailable by the author if desired
|
||||
isAvailable Boolean @default(true)
|
||||
// Not needed but makes lookups faster
|
||||
isApproved Boolean @default(false)
|
||||
StoreListing StoreListing? @relation(fields: [storeListingId], references: [id], onDelete: Cascade)
|
||||
storeListingId String?
|
||||
StoreListingSubmission StoreListingSubmission[]
|
||||
|
||||
// Reviews are on a specific version, but then aggregated up to the listing.
|
||||
// This allows us to provide a review filter to current version of the agent.
|
||||
StoreListingReview StoreListingReview[]
|
||||
|
||||
@@unique([agentId, agentVersion])
|
||||
@@index([agentId, agentVersion, isApproved])
|
||||
}
|
||||
|
||||
model StoreListingReview {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
storeListingVersionId String
|
||||
StoreListingVersion StoreListingVersion @relation(fields: [storeListingVersionId], references: [id], onDelete: Cascade)
|
||||
|
||||
reviewByUserId String
|
||||
ReviewByUser User @relation(fields: [reviewByUserId], references: [id])
|
||||
|
||||
score Int
|
||||
comments String?
|
||||
|
||||
@@unique([storeListingVersionId, reviewByUserId])
|
||||
@@index([storeListingVersionId])
|
||||
}
|
||||
|
||||
enum SubmissionStatus {
|
||||
DAFT
|
||||
PENDING
|
||||
APPROVED
|
||||
REJECTED
|
||||
}
|
||||
|
||||
model StoreListingSubmission {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
storeListingId String
|
||||
StoreListing StoreListing @relation(fields: [storeListingId], references: [id], onDelete: Cascade)
|
||||
|
||||
storeListingVersionId String
|
||||
StoreListingVersion StoreListingVersion @relation(fields: [storeListingVersionId], references: [id], onDelete: Cascade)
|
||||
|
||||
reviewerId String
|
||||
Reviewer User @relation(fields: [reviewerId], references: [id])
|
||||
|
||||
Status SubmissionStatus @default(PENDING)
|
||||
reviewComments String?
|
||||
|
||||
@@index([storeListingId])
|
||||
@@index([Status])
|
||||
}
|
||||
|
||||
enum APIKeyPermission {
|
||||
EXECUTE_GRAPH // Can execute agent graphs
|
||||
READ_GRAPH // Can get graph versions and details
|
||||
@@ -338,4 +635,4 @@ enum APIKeyStatus {
|
||||
ACTIVE
|
||||
REVOKED
|
||||
SUSPENDED
|
||||
}
|
||||
}
|
||||
457
autogpt_platform/backend/test/test_data_creator.py
Normal file
457
autogpt_platform/backend/test/test_data_creator.py
Normal file
@@ -0,0 +1,457 @@
|
||||
import asyncio
|
||||
import random
|
||||
from datetime import datetime
|
||||
|
||||
import prisma.enums
|
||||
from faker import Faker
|
||||
from prisma import Prisma
|
||||
|
||||
faker = Faker()
|
||||
|
||||
# Constants for data generation limits
|
||||
|
||||
# Base entities
|
||||
NUM_USERS = 100 # Creates 100 user records
|
||||
NUM_AGENT_BLOCKS = 100 # Creates 100 agent block templates
|
||||
|
||||
# Per-user entities
|
||||
MIN_GRAPHS_PER_USER = 1 # Each user will have between 1-5 graphs
|
||||
MAX_GRAPHS_PER_USER = 5 # Total graphs: 500-2500 (NUM_USERS * MIN/MAX_GRAPHS)
|
||||
|
||||
# Per-graph entities
|
||||
MIN_NODES_PER_GRAPH = 2 # Each graph will have between 2-5 nodes
|
||||
MAX_NODES_PER_GRAPH = (
|
||||
5 # Total nodes: 1000-2500 (GRAPHS_PER_USER * NUM_USERS * MIN/MAX_NODES)
|
||||
)
|
||||
|
||||
# Additional per-user entities
|
||||
MIN_PRESETS_PER_USER = 1 # Each user will have between 1-2 presets
|
||||
MAX_PRESETS_PER_USER = 5 # Total presets: 500-2500 (NUM_USERS * MIN/MAX_PRESETS)
|
||||
MIN_AGENTS_PER_USER = 1 # Each user will have between 1-2 agents
|
||||
MAX_AGENTS_PER_USER = 10 # Total agents: 500-5000 (NUM_USERS * MIN/MAX_AGENTS)
|
||||
|
||||
# Execution and review records
|
||||
MIN_EXECUTIONS_PER_GRAPH = 1 # Each graph will have between 1-5 execution records
|
||||
MAX_EXECUTIONS_PER_GRAPH = (
|
||||
20 # Total executions: 1000-5000 (TOTAL_GRAPHS * MIN/MAX_EXECUTIONS)
|
||||
)
|
||||
MIN_REVIEWS_PER_VERSION = 1 # Each version will have between 1-3 reviews
|
||||
MAX_REVIEWS_PER_VERSION = 5 # Total reviews depends on number of versions created
|
||||
|
||||
|
||||
def get_image():
|
||||
url = faker.image_url()
|
||||
while "placekitten.com" in url:
|
||||
url = faker.image_url()
|
||||
return url
|
||||
|
||||
|
||||
async def main():
|
||||
db = Prisma()
|
||||
await db.connect()
|
||||
|
||||
# Insert Users
|
||||
print(f"Inserting {NUM_USERS} users")
|
||||
users = []
|
||||
for _ in range(NUM_USERS):
|
||||
user = await db.user.create(
|
||||
data={
|
||||
"id": str(faker.uuid4()),
|
||||
"email": faker.unique.email(),
|
||||
"name": faker.name(),
|
||||
"metadata": prisma.Json({}),
|
||||
"integrations": "",
|
||||
}
|
||||
)
|
||||
users.append(user)
|
||||
|
||||
# Insert AgentBlocks
|
||||
agent_blocks = []
|
||||
print(f"Inserting {NUM_AGENT_BLOCKS} agent blocks")
|
||||
for _ in range(NUM_AGENT_BLOCKS):
|
||||
block = await db.agentblock.create(
|
||||
data={
|
||||
"name": f"{faker.word()}_{str(faker.uuid4())[:8]}",
|
||||
"inputSchema": "{}",
|
||||
"outputSchema": "{}",
|
||||
}
|
||||
)
|
||||
agent_blocks.append(block)
|
||||
|
||||
# Insert AgentGraphs
|
||||
agent_graphs = []
|
||||
print(f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER} agent graphs")
|
||||
for user in users:
|
||||
for _ in range(
|
||||
random.randint(MIN_GRAPHS_PER_USER, MAX_GRAPHS_PER_USER)
|
||||
): # Adjust the range to create more graphs per user if desired
|
||||
graph = await db.agentgraph.create(
|
||||
data={
|
||||
"name": faker.sentence(nb_words=3),
|
||||
"description": faker.text(max_nb_chars=200),
|
||||
"userId": user.id,
|
||||
"isActive": True,
|
||||
"isTemplate": False,
|
||||
}
|
||||
)
|
||||
agent_graphs.append(graph)
|
||||
|
||||
# Insert AgentNodes
|
||||
agent_nodes = []
|
||||
print(
|
||||
f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER * MAX_NODES_PER_GRAPH} agent nodes"
|
||||
)
|
||||
for graph in agent_graphs:
|
||||
num_nodes = random.randint(MIN_NODES_PER_GRAPH, MAX_NODES_PER_GRAPH)
|
||||
for _ in range(num_nodes): # Create 5 AgentNodes per graph
|
||||
block = random.choice(agent_blocks)
|
||||
node = await db.agentnode.create(
|
||||
data={
|
||||
"agentBlockId": block.id,
|
||||
"agentGraphId": graph.id,
|
||||
"agentGraphVersion": graph.version,
|
||||
"constantInput": "{}",
|
||||
"metadata": "{}",
|
||||
}
|
||||
)
|
||||
agent_nodes.append(node)
|
||||
|
||||
# Insert AgentPresets
|
||||
agent_presets = []
|
||||
print(f"Inserting {NUM_USERS * MAX_PRESETS_PER_USER} agent presets")
|
||||
for user in users:
|
||||
num_presets = random.randint(MIN_PRESETS_PER_USER, MAX_PRESETS_PER_USER)
|
||||
for _ in range(num_presets): # Create 1 AgentPreset per user
|
||||
graph = random.choice(agent_graphs)
|
||||
preset = await db.agentpreset.create(
|
||||
data={
|
||||
"name": faker.sentence(nb_words=3),
|
||||
"description": faker.text(max_nb_chars=200),
|
||||
"userId": user.id,
|
||||
"agentId": graph.id,
|
||||
"agentVersion": graph.version,
|
||||
"isActive": True,
|
||||
}
|
||||
)
|
||||
agent_presets.append(preset)
|
||||
|
||||
# Insert UserAgents
|
||||
user_agents = []
|
||||
print(f"Inserting {NUM_USERS * MAX_AGENTS_PER_USER} user agents")
|
||||
for user in users:
|
||||
num_agents = random.randint(MIN_AGENTS_PER_USER, MAX_AGENTS_PER_USER)
|
||||
for _ in range(num_agents): # Create 1 UserAgent per user
|
||||
graph = random.choice(agent_graphs)
|
||||
preset = random.choice(agent_presets)
|
||||
user_agent = await db.useragent.create(
|
||||
data={
|
||||
"userId": user.id,
|
||||
"agentId": graph.id,
|
||||
"agentVersion": graph.version,
|
||||
"agentPresetId": preset.id,
|
||||
"isFavorite": random.choice([True, False]),
|
||||
"isCreatedByUser": random.choice([True, False]),
|
||||
"isArchived": random.choice([True, False]),
|
||||
"isDeleted": random.choice([True, False]),
|
||||
}
|
||||
)
|
||||
user_agents.append(user_agent)
|
||||
|
||||
# Insert AgentGraphExecutions
|
||||
# Insert AgentGraphExecutions
|
||||
agent_graph_executions = []
|
||||
print(
|
||||
f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER * MAX_EXECUTIONS_PER_GRAPH} agent graph executions"
|
||||
)
|
||||
graph_execution_data = []
|
||||
for graph in agent_graphs:
|
||||
user = random.choice(users)
|
||||
num_executions = random.randint(
|
||||
MIN_EXECUTIONS_PER_GRAPH, MAX_EXECUTIONS_PER_GRAPH
|
||||
)
|
||||
for _ in range(num_executions):
|
||||
matching_presets = [p for p in agent_presets if p.agentId == graph.id]
|
||||
preset = (
|
||||
random.choice(matching_presets)
|
||||
if matching_presets and random.random() < 0.5
|
||||
else None
|
||||
)
|
||||
|
||||
graph_execution_data.append(
|
||||
{
|
||||
"agentGraphId": graph.id,
|
||||
"agentGraphVersion": graph.version,
|
||||
"userId": user.id,
|
||||
"executionStatus": prisma.enums.AgentExecutionStatus.COMPLETED,
|
||||
"startedAt": faker.date_time_this_year(),
|
||||
"agentPresetId": preset.id if preset else None,
|
||||
}
|
||||
)
|
||||
|
||||
agent_graph_executions = await db.agentgraphexecution.create_many(
|
||||
data=graph_execution_data
|
||||
)
|
||||
# Need to fetch the created records since create_many doesn't return them
|
||||
agent_graph_executions = await db.agentgraphexecution.find_many()
|
||||
|
||||
# Insert AgentNodeExecutions
|
||||
print(
|
||||
f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER * MAX_EXECUTIONS_PER_GRAPH} agent node executions"
|
||||
)
|
||||
node_execution_data = []
|
||||
for execution in agent_graph_executions:
|
||||
nodes = [
|
||||
node for node in agent_nodes if node.agentGraphId == execution.agentGraphId
|
||||
]
|
||||
for node in nodes:
|
||||
node_execution_data.append(
|
||||
{
|
||||
"agentGraphExecutionId": execution.id,
|
||||
"agentNodeId": node.id,
|
||||
"executionStatus": prisma.enums.AgentExecutionStatus.COMPLETED,
|
||||
"addedTime": datetime.now(),
|
||||
}
|
||||
)
|
||||
|
||||
agent_node_executions = await db.agentnodeexecution.create_many(
|
||||
data=node_execution_data
|
||||
)
|
||||
# Need to fetch the created records since create_many doesn't return them
|
||||
agent_node_executions = await db.agentnodeexecution.find_many()
|
||||
|
||||
# Insert AgentNodeExecutionInputOutput
|
||||
print(
|
||||
f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER * MAX_EXECUTIONS_PER_GRAPH} agent node execution input/outputs"
|
||||
)
|
||||
input_output_data = []
|
||||
for node_execution in agent_node_executions:
|
||||
# Input data
|
||||
input_output_data.append(
|
||||
{
|
||||
"name": "input1",
|
||||
"data": "{}",
|
||||
"time": datetime.now(),
|
||||
"referencedByInputExecId": node_execution.id,
|
||||
}
|
||||
)
|
||||
# Output data
|
||||
input_output_data.append(
|
||||
{
|
||||
"name": "output1",
|
||||
"data": "{}",
|
||||
"time": datetime.now(),
|
||||
"referencedByOutputExecId": node_execution.id,
|
||||
}
|
||||
)
|
||||
|
||||
await db.agentnodeexecutioninputoutput.create_many(data=input_output_data)
|
||||
|
||||
# Insert AgentGraphExecutionSchedules
|
||||
agent_graph_execution_schedules = []
|
||||
print(
|
||||
f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER} agent graph execution schedules"
|
||||
)
|
||||
for graph in agent_graphs:
|
||||
user = random.choice(users)
|
||||
schedule = await db.agentgraphexecutionschedule.create(
|
||||
data={
|
||||
"id": str(faker.uuid4()),
|
||||
"agentGraphId": graph.id,
|
||||
"agentGraphVersion": graph.version,
|
||||
"schedule": "* * * * *",
|
||||
"isEnabled": True,
|
||||
"inputData": "{}",
|
||||
"userId": user.id,
|
||||
"lastUpdated": datetime.now(),
|
||||
}
|
||||
)
|
||||
agent_graph_execution_schedules.append(schedule)
|
||||
|
||||
# Insert AgentNodeLinks
|
||||
print(f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER} agent node links")
|
||||
for graph in agent_graphs:
|
||||
nodes = [node for node in agent_nodes if node.agentGraphId == graph.id]
|
||||
if len(nodes) >= 2:
|
||||
source_node = nodes[0]
|
||||
sink_node = nodes[1]
|
||||
await db.agentnodelink.create(
|
||||
data={
|
||||
"agentNodeSourceId": source_node.id,
|
||||
"sourceName": "output1",
|
||||
"agentNodeSinkId": sink_node.id,
|
||||
"sinkName": "input1",
|
||||
"isStatic": False,
|
||||
}
|
||||
)
|
||||
|
||||
# Insert AnalyticsDetails
|
||||
print(f"Inserting {NUM_USERS} analytics details")
|
||||
for user in users:
|
||||
for _ in range(1):
|
||||
await db.analyticsdetails.create(
|
||||
data={
|
||||
"userId": user.id,
|
||||
"type": faker.word(),
|
||||
"data": prisma.Json({}),
|
||||
"dataIndex": faker.word(),
|
||||
}
|
||||
)
|
||||
|
||||
# Insert AnalyticsMetrics
|
||||
print(f"Inserting {NUM_USERS} analytics metrics")
|
||||
for user in users:
|
||||
for _ in range(1):
|
||||
await db.analyticsmetrics.create(
|
||||
data={
|
||||
"userId": user.id,
|
||||
"analyticMetric": faker.word(),
|
||||
"value": random.uniform(0, 100),
|
||||
"dataString": faker.word(),
|
||||
}
|
||||
)
|
||||
|
||||
# Insert UserBlockCredit
|
||||
print(f"Inserting {NUM_USERS} user block credits")
|
||||
for user in users:
|
||||
for _ in range(1):
|
||||
block = random.choice(agent_blocks)
|
||||
await db.userblockcredit.create(
|
||||
data={
|
||||
"transactionKey": str(faker.uuid4()),
|
||||
"userId": user.id,
|
||||
"blockId": block.id,
|
||||
"amount": random.randint(1, 100),
|
||||
"type": (
|
||||
prisma.enums.UserBlockCreditType.TOP_UP
|
||||
if random.random() < 0.5
|
||||
else prisma.enums.UserBlockCreditType.USAGE
|
||||
),
|
||||
"metadata": prisma.Json({}),
|
||||
}
|
||||
)
|
||||
|
||||
# Insert Profiles
|
||||
profiles = []
|
||||
print(f"Inserting {NUM_USERS} profiles")
|
||||
for user in users:
|
||||
profile = await db.profile.create(
|
||||
data={
|
||||
"userId": user.id,
|
||||
"name": user.name or faker.name(),
|
||||
"username": faker.unique.user_name(),
|
||||
"description": faker.text(),
|
||||
"links": [faker.url() for _ in range(3)],
|
||||
"avatarUrl": get_image(),
|
||||
}
|
||||
)
|
||||
profiles.append(profile)
|
||||
|
||||
# Insert StoreListings
|
||||
store_listings = []
|
||||
print(f"Inserting {NUM_USERS} store listings")
|
||||
for graph in agent_graphs:
|
||||
user = random.choice(users)
|
||||
listing = await db.storelisting.create(
|
||||
data={
|
||||
"agentId": graph.id,
|
||||
"agentVersion": graph.version,
|
||||
"owningUserId": user.id,
|
||||
"isApproved": random.choice([True, False]),
|
||||
}
|
||||
)
|
||||
store_listings.append(listing)
|
||||
|
||||
# Insert StoreListingVersions
|
||||
store_listing_versions = []
|
||||
print(f"Inserting {NUM_USERS} store listing versions")
|
||||
for listing in store_listings:
|
||||
graph = [g for g in agent_graphs if g.id == listing.agentId][0]
|
||||
version = await db.storelistingversion.create(
|
||||
data={
|
||||
"agentId": graph.id,
|
||||
"agentVersion": graph.version,
|
||||
"slug": faker.slug(),
|
||||
"name": graph.name or faker.sentence(nb_words=3),
|
||||
"subHeading": faker.sentence(),
|
||||
"videoUrl": faker.url(),
|
||||
"imageUrls": [get_image() for _ in range(3)],
|
||||
"description": faker.text(),
|
||||
"categories": [faker.word() for _ in range(3)],
|
||||
"isFeatured": random.choice([True, False]),
|
||||
"isAvailable": True,
|
||||
"isApproved": random.choice([True, False]),
|
||||
"storeListingId": listing.id,
|
||||
}
|
||||
)
|
||||
store_listing_versions.append(version)
|
||||
|
||||
# Insert StoreListingReviews
|
||||
print(f"Inserting {NUM_USERS * MAX_REVIEWS_PER_VERSION} store listing reviews")
|
||||
for version in store_listing_versions:
|
||||
# Create a copy of users list and shuffle it to avoid duplicates
|
||||
available_reviewers = users.copy()
|
||||
random.shuffle(available_reviewers)
|
||||
|
||||
# Limit number of reviews to available unique reviewers
|
||||
num_reviews = min(
|
||||
random.randint(MIN_REVIEWS_PER_VERSION, MAX_REVIEWS_PER_VERSION),
|
||||
len(available_reviewers),
|
||||
)
|
||||
|
||||
# Take only the first num_reviews reviewers
|
||||
for reviewer in available_reviewers[:num_reviews]:
|
||||
await db.storelistingreview.create(
|
||||
data={
|
||||
"storeListingVersionId": version.id,
|
||||
"reviewByUserId": reviewer.id,
|
||||
"score": random.randint(1, 5),
|
||||
"comments": faker.text(),
|
||||
}
|
||||
)
|
||||
|
||||
# Insert StoreListingSubmissions
|
||||
print(f"Inserting {NUM_USERS} store listing submissions")
|
||||
for listing in store_listings:
|
||||
version = random.choice(store_listing_versions)
|
||||
reviewer = random.choice(users)
|
||||
status: prisma.enums.SubmissionStatus = random.choice(
|
||||
[
|
||||
prisma.enums.SubmissionStatus.PENDING,
|
||||
prisma.enums.SubmissionStatus.APPROVED,
|
||||
prisma.enums.SubmissionStatus.REJECTED,
|
||||
]
|
||||
)
|
||||
await db.storelistingsubmission.create(
|
||||
data={
|
||||
"storeListingId": listing.id,
|
||||
"storeListingVersionId": version.id,
|
||||
"reviewerId": reviewer.id,
|
||||
"Status": status,
|
||||
"reviewComments": faker.text(),
|
||||
}
|
||||
)
|
||||
|
||||
# Insert APIKeys
|
||||
print(f"Inserting {NUM_USERS} api keys")
|
||||
for user in users:
|
||||
await db.apikey.create(
|
||||
data={
|
||||
"name": faker.word(),
|
||||
"prefix": str(faker.uuid4())[:8],
|
||||
"postfix": str(faker.uuid4())[-8:],
|
||||
"key": str(faker.sha256()),
|
||||
"status": prisma.enums.APIKeyStatus.ACTIVE,
|
||||
"permissions": [
|
||||
prisma.enums.APIKeyPermission.EXECUTE_GRAPH,
|
||||
prisma.enums.APIKeyPermission.READ_GRAPH,
|
||||
],
|
||||
"description": faker.text(),
|
||||
"userId": user.id,
|
||||
}
|
||||
)
|
||||
|
||||
await db.disconnect()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -6,6 +6,11 @@ NEXT_PUBLIC_LAUNCHDARKLY_ENABLED=false
|
||||
NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID=
|
||||
NEXT_PUBLIC_APP_ENV=dev
|
||||
|
||||
## Locale settings
|
||||
|
||||
NEXT_PUBLIC_DEFAULT_LOCALE=en
|
||||
NEXT_PUBLIC_LOCALES=en,es
|
||||
|
||||
## Supabase credentials
|
||||
|
||||
NEXT_PUBLIC_SUPABASE_URL=http://localhost:8000
|
||||
|
||||
3
autogpt_platform/frontend/.gitignore
vendored
3
autogpt_platform/frontend/.gitignore
vendored
@@ -45,3 +45,6 @@ node_modules/
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
*.ignore.*
|
||||
*.ign.*
|
||||
.cursorrules
|
||||
@@ -3,12 +3,15 @@ import type { StorybookConfig } from "@storybook/nextjs";
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
addons: [
|
||||
"@storybook/addon-a11y",
|
||||
"@storybook/addon-onboarding",
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@chromatic-com/storybook",
|
||||
"@storybook/addon-interactions",
|
||||
],
|
||||
features: {
|
||||
experimentalRSC: true,
|
||||
},
|
||||
framework: {
|
||||
name: "@storybook/nextjs",
|
||||
options: {},
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import type { Preview } from "@storybook/react";
|
||||
import { initialize, mswLoader } from "msw-storybook-addon";
|
||||
import "../src/app/globals.css";
|
||||
|
||||
// Initialize MSW
|
||||
initialize();
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
@@ -10,6 +17,7 @@ const preview: Preview = {
|
||||
},
|
||||
},
|
||||
},
|
||||
loaders: [mswLoader],
|
||||
};
|
||||
|
||||
export default preview;
|
||||
|
||||
22
autogpt_platform/frontend/.storybook/test-runner.ts
Normal file
22
autogpt_platform/frontend/.storybook/test-runner.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { TestRunnerConfig } from "@storybook/test-runner";
|
||||
import { injectAxe, checkA11y } from "axe-playwright";
|
||||
|
||||
/*
|
||||
* See https://storybook.js.org/docs/writing-tests/test-runner#test-hook-api
|
||||
* to learn more about the test-runner hooks API.
|
||||
*/
|
||||
const config: TestRunnerConfig = {
|
||||
async preVisit(page) {
|
||||
await injectAxe(page);
|
||||
},
|
||||
async postVisit(page) {
|
||||
await checkA11y(page, "#storybook-root", {
|
||||
detailedReport: true,
|
||||
detailedReportOptions: {
|
||||
html: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -7,18 +7,21 @@ RUN --mount=type=cache,target=/usr/local/share/.cache yarn install --frozen-lock
|
||||
# Dev stage
|
||||
FROM base AS dev
|
||||
ENV NODE_ENV=development
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
COPY autogpt_platform/frontend/ .
|
||||
EXPOSE 3000
|
||||
CMD ["yarn", "run", "dev"]
|
||||
CMD ["yarn", "run", "dev", "--hostname", "0.0.0.0"]
|
||||
|
||||
# Build stage for prod
|
||||
FROM base AS build
|
||||
COPY autogpt_platform/frontend/ .
|
||||
ENV SKIP_STORYBOOK_TESTS=true
|
||||
RUN yarn build
|
||||
|
||||
# Prod stage - based on NextJS reference Dockerfile https://github.com/vercel/next.js/blob/64271354533ed16da51be5dce85f0dbd15f17517/examples/with-docker/Dockerfile
|
||||
FROM node:21-alpine AS prod
|
||||
ENV NODE_ENV=production
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
|
||||
@@ -3,16 +3,16 @@ import { withSentryConfig } from "@sentry/nextjs";
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
images: {
|
||||
domains: ["images.unsplash.com"],
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: "/monitor", // FIXME: Remove after 2024-09-01
|
||||
destination: "/",
|
||||
permanent: false,
|
||||
},
|
||||
];
|
||||
domains: [
|
||||
"images.unsplash.com",
|
||||
"ddz4ak4pa3d19.cloudfront.net",
|
||||
"upload.wikimedia.org",
|
||||
"storage.googleapis.com",
|
||||
|
||||
"picsum.photos", // for placeholder images
|
||||
"dummyimage.com", // for placeholder images
|
||||
"placekitten.com", // for placeholder images
|
||||
],
|
||||
},
|
||||
output: "standalone",
|
||||
// TODO: Re-enable TypeScript checks once current issues are resolved
|
||||
@@ -46,7 +46,7 @@ export default withSentryConfig(nextConfig, {
|
||||
// This can increase your server load as well as your hosting bill.
|
||||
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
|
||||
// side errors will fail.
|
||||
tunnelRoute: "/monitoring",
|
||||
tunnelRoute: "/store",
|
||||
|
||||
// Hides source maps from generated client bundles
|
||||
hideSourceMaps: true,
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev:nosentry": "export NODE_ENV=development && export DISABLE_SENTRY=true && next dev",
|
||||
"dev:test": "export NODE_ENV=test && next dev",
|
||||
"build": "next build",
|
||||
"dev:nosentry": "NODE_ENV=development && DISABLE_SENTRY=true && next dev",
|
||||
"dev:test": "NODE_ENV=test && next dev",
|
||||
"build": "SKIP_STORYBOOK_TESTS=true next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint && prettier --check .",
|
||||
"format": "prettier --write .",
|
||||
@@ -50,13 +50,17 @@
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"@xyflow/react": "^12.3.6",
|
||||
"ajv": "^8.17.1",
|
||||
"boring-avatars": "^1.11.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
"cookie": "1.0.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"elliptic": "6.6.1",
|
||||
"elliptic": "6.6.0",
|
||||
"embla-carousel-react": "^8.3.0",
|
||||
"framer-motion": "^11.11.9",
|
||||
"geist": "^1.3.1",
|
||||
"launchdarkly-react-client-sdk": "^3.6.0",
|
||||
"lucide-react": "^0.468.0",
|
||||
"moment": "^2.30.1",
|
||||
@@ -78,24 +82,30 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^3.2.2",
|
||||
"@playwright/test": "^1.49.0",
|
||||
"@storybook/addon-essentials": "^8.4.5",
|
||||
"@storybook/addon-interactions": "^8.4.5",
|
||||
"@storybook/addon-links": "^8.4.5",
|
||||
"@storybook/addon-onboarding": "^8.4.5",
|
||||
"@storybook/blocks": "^8.4.5",
|
||||
"@storybook/nextjs": "^8.4.5",
|
||||
"@playwright/test": "^1.48.2",
|
||||
"@storybook/addon-a11y": "^8.3.5",
|
||||
"@storybook/addon-essentials": "^8.4.2",
|
||||
"@storybook/addon-interactions": "^8.4.2",
|
||||
"@storybook/addon-links": "^8.4.2",
|
||||
"@storybook/addon-onboarding": "^8.4.2",
|
||||
"@storybook/blocks": "^8.4.2",
|
||||
"@storybook/nextjs": "^8.4.2",
|
||||
"@storybook/react": "^8.3.5",
|
||||
"@storybook/test": "^8.3.5",
|
||||
"@storybook/test-runner": "^0.19.1",
|
||||
"@types/node": "^22.9.3",
|
||||
"@types/negotiator": "^0.6.3",
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-modal": "^3.16.3",
|
||||
"concurrently": "^9.1.0",
|
||||
"axe-playwright": "^2.0.3",
|
||||
"chromatic": "^11.12.5",
|
||||
"concurrently": "^9.0.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "15.0.3",
|
||||
"eslint-plugin-storybook": "^0.11.1",
|
||||
"eslint-plugin-storybook": "^0.11.0",
|
||||
"msw": "^2.5.2",
|
||||
"msw-storybook-addon": "^2.0.3",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
||||
@@ -103,5 +113,10 @@
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
||||
"msw": {
|
||||
"workerDirectory": [
|
||||
"public"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export default defineConfig({
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
screenshot: 'only-on-failure',
|
||||
bypassCSP: true,
|
||||
},
|
||||
|
||||
@@ -50,17 +51,17 @@ export default defineConfig({
|
||||
use: { ...devices["Desktop Safari"] },
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
// /* Test against mobile viewports. */
|
||||
// // {
|
||||
// // name: 'Mobile Chrome',
|
||||
// // use: { ...devices['Pixel 5'] },
|
||||
// // },
|
||||
// // {
|
||||
// // name: 'Mobile Safari',
|
||||
// // use: { ...devices['iPhone 12'] },
|
||||
// // },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// /* Test against branded browsers. */
|
||||
{
|
||||
name: "Microsoft Edge",
|
||||
use: { ...devices["Desktop Edge"], channel: "msedge" },
|
||||
|
||||
307
autogpt_platform/frontend/public/mockServiceWorker.js
Normal file
307
autogpt_platform/frontend/public/mockServiceWorker.js
Normal file
@@ -0,0 +1,307 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
|
||||
/**
|
||||
* Mock Service Worker.
|
||||
* @see https://github.com/mswjs/msw
|
||||
* - Please do NOT modify this file.
|
||||
* - Please do NOT serve this file on production.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = '2.6.8'
|
||||
const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
|
||||
self.addEventListener('install', function () {
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
self.addEventListener('activate', function (event) {
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
self.addEventListener('message', async function (event) {
|
||||
const clientId = event.source.id
|
||||
|
||||
if (!clientId || !self.clients) {
|
||||
return
|
||||
}
|
||||
|
||||
const client = await self.clients.get(clientId)
|
||||
|
||||
if (!client) {
|
||||
return
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
switch (event.data) {
|
||||
case 'KEEPALIVE_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: 'KEEPALIVE_RESPONSE',
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'INTEGRITY_CHECK_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: 'INTEGRITY_CHECK_RESPONSE',
|
||||
payload: {
|
||||
packageVersion: PACKAGE_VERSION,
|
||||
checksum: INTEGRITY_CHECKSUM,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'MOCK_ACTIVATE': {
|
||||
activeClientIds.add(clientId)
|
||||
|
||||
sendToClient(client, {
|
||||
type: 'MOCKING_ENABLED',
|
||||
payload: {
|
||||
client: {
|
||||
id: client.id,
|
||||
frameType: client.frameType,
|
||||
},
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'MOCK_DEACTIVATE': {
|
||||
activeClientIds.delete(clientId)
|
||||
break
|
||||
}
|
||||
|
||||
case 'CLIENT_CLOSED': {
|
||||
activeClientIds.delete(clientId)
|
||||
|
||||
const remainingClients = allClients.filter((client) => {
|
||||
return client.id !== clientId
|
||||
})
|
||||
|
||||
// Unregister itself when there are no more clients
|
||||
if (remainingClients.length === 0) {
|
||||
self.registration.unregister()
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', function (event) {
|
||||
const { request } = event
|
||||
|
||||
// Bypass navigation requests.
|
||||
if (request.mode === 'navigate') {
|
||||
return
|
||||
}
|
||||
|
||||
// Opening the DevTools triggers the "only-if-cached" request
|
||||
// that cannot be handled by the worker. Bypass such requests.
|
||||
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
|
||||
return
|
||||
}
|
||||
|
||||
// Bypass all requests when there are no active clients.
|
||||
// Prevents the self-unregistered worked from handling requests
|
||||
// after it's been deleted (still remains active until the next reload).
|
||||
if (activeClientIds.size === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Generate unique request ID.
|
||||
const requestId = crypto.randomUUID()
|
||||
event.respondWith(handleRequest(event, requestId))
|
||||
})
|
||||
|
||||
async function handleRequest(event, requestId) {
|
||||
const client = await resolveMainClient(event)
|
||||
const response = await getResponse(event, client, requestId)
|
||||
|
||||
// Send back the response clone for the "response:*" life-cycle events.
|
||||
// Ensure MSW is active and ready to handle the message, otherwise
|
||||
// this message will pend indefinitely.
|
||||
if (client && activeClientIds.has(client.id)) {
|
||||
;(async function () {
|
||||
const responseClone = response.clone()
|
||||
|
||||
sendToClient(
|
||||
client,
|
||||
{
|
||||
type: 'RESPONSE',
|
||||
payload: {
|
||||
requestId,
|
||||
isMockedResponse: IS_MOCKED_RESPONSE in response,
|
||||
type: responseClone.type,
|
||||
status: responseClone.status,
|
||||
statusText: responseClone.statusText,
|
||||
body: responseClone.body,
|
||||
headers: Object.fromEntries(responseClone.headers.entries()),
|
||||
},
|
||||
},
|
||||
[responseClone.body],
|
||||
)
|
||||
})()
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// Resolve the main client for the given event.
|
||||
// Client that issues a request doesn't necessarily equal the client
|
||||
// that registered the worker. It's with the latter the worker should
|
||||
// communicate with during the response resolving phase.
|
||||
async function resolveMainClient(event) {
|
||||
const client = await self.clients.get(event.clientId)
|
||||
|
||||
if (activeClientIds.has(event.clientId)) {
|
||||
return client
|
||||
}
|
||||
|
||||
if (client?.frameType === 'top-level') {
|
||||
return client
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
return allClients
|
||||
.filter((client) => {
|
||||
// Get only those clients that are currently visible.
|
||||
return client.visibilityState === 'visible'
|
||||
})
|
||||
.find((client) => {
|
||||
// Find the client ID that's recorded in the
|
||||
// set of clients that have registered the worker.
|
||||
return activeClientIds.has(client.id)
|
||||
})
|
||||
}
|
||||
|
||||
async function getResponse(event, client, requestId) {
|
||||
const { request } = event
|
||||
|
||||
// Clone the request because it might've been already used
|
||||
// (i.e. its body has been read and sent to the client).
|
||||
const requestClone = request.clone()
|
||||
|
||||
function passthrough() {
|
||||
// Cast the request headers to a new Headers instance
|
||||
// so the headers can be manipulated with.
|
||||
const headers = new Headers(requestClone.headers)
|
||||
|
||||
// Remove the "accept" header value that marked this request as passthrough.
|
||||
// This prevents request alteration and also keeps it compliant with the
|
||||
// user-defined CORS policies.
|
||||
const acceptHeader = headers.get('accept')
|
||||
if (acceptHeader) {
|
||||
const values = acceptHeader.split(',').map((value) => value.trim())
|
||||
const filteredValues = values.filter(
|
||||
(value) => value !== 'msw/passthrough',
|
||||
)
|
||||
|
||||
if (filteredValues.length > 0) {
|
||||
headers.set('accept', filteredValues.join(', '))
|
||||
} else {
|
||||
headers.delete('accept')
|
||||
}
|
||||
}
|
||||
|
||||
return fetch(requestClone, { headers })
|
||||
}
|
||||
|
||||
// Bypass mocking when the client is not active.
|
||||
if (!client) {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Bypass initial page load requests (i.e. static assets).
|
||||
// The absence of the immediate/parent client in the map of the active clients
|
||||
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
|
||||
// and is not ready to handle requests.
|
||||
if (!activeClientIds.has(client.id)) {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Notify the client that a request has been intercepted.
|
||||
const requestBuffer = await request.arrayBuffer()
|
||||
const clientMessage = await sendToClient(
|
||||
client,
|
||||
{
|
||||
type: 'REQUEST',
|
||||
payload: {
|
||||
id: requestId,
|
||||
url: request.url,
|
||||
mode: request.mode,
|
||||
method: request.method,
|
||||
headers: Object.fromEntries(request.headers.entries()),
|
||||
cache: request.cache,
|
||||
credentials: request.credentials,
|
||||
destination: request.destination,
|
||||
integrity: request.integrity,
|
||||
redirect: request.redirect,
|
||||
referrer: request.referrer,
|
||||
referrerPolicy: request.referrerPolicy,
|
||||
body: requestBuffer,
|
||||
keepalive: request.keepalive,
|
||||
},
|
||||
},
|
||||
[requestBuffer],
|
||||
)
|
||||
|
||||
switch (clientMessage.type) {
|
||||
case 'MOCK_RESPONSE': {
|
||||
return respondWithMock(clientMessage.data)
|
||||
}
|
||||
|
||||
case 'PASSTHROUGH': {
|
||||
return passthrough()
|
||||
}
|
||||
}
|
||||
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
function sendToClient(client, message, transferrables = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const channel = new MessageChannel()
|
||||
|
||||
channel.port1.onmessage = (event) => {
|
||||
if (event.data && event.data.error) {
|
||||
return reject(event.data.error)
|
||||
}
|
||||
|
||||
resolve(event.data)
|
||||
}
|
||||
|
||||
client.postMessage(
|
||||
message,
|
||||
[channel.port2].concat(transferrables.filter(Boolean)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async function respondWithMock(response) {
|
||||
// Setting response status code to 0 is a no-op.
|
||||
// However, when responding with a "Response.error()", the produced Response
|
||||
// instance will have status code set to 0. Since it's not possible to create
|
||||
// a Response instance with status code 0, handle that use-case separately.
|
||||
if (response.status === 0) {
|
||||
return Response.error()
|
||||
}
|
||||
|
||||
const mockedResponse = new Response(response.body, response)
|
||||
|
||||
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
|
||||
value: true,
|
||||
enumerable: true,
|
||||
})
|
||||
|
||||
return mockedResponse
|
||||
}
|
||||
@@ -9,8 +9,7 @@ export default function Home() {
|
||||
return (
|
||||
<FlowEditor
|
||||
className="flow-container"
|
||||
flowID={query.get("flowID") ?? query.get("templateID") ?? undefined}
|
||||
template={!!query.get("templateID")}
|
||||
flowID={query.get("flowID") ?? undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
22
autogpt_platform/frontend/src/app/dictionaries/en.json
Normal file
22
autogpt_platform/frontend/src/app/dictionaries/en.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"auth": {
|
||||
"signIn": "Sign In",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Submit",
|
||||
"error": "Invalid login credentials"
|
||||
},
|
||||
"dashboard": {
|
||||
"welcome": "Welcome to your dashboard",
|
||||
"stats": "Your Stats",
|
||||
"recentActivity": "Recent Activity"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Admin Dashboard",
|
||||
"users": "Users Management",
|
||||
"settings": "System Settings"
|
||||
},
|
||||
"home": {
|
||||
"welcome": "Welcome to the Home Page"
|
||||
}
|
||||
}
|
||||
22
autogpt_platform/frontend/src/app/dictionaries/es.json
Normal file
22
autogpt_platform/frontend/src/app/dictionaries/es.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"auth": {
|
||||
"signIn": "Iniciar Sesión",
|
||||
"email": "Correo electrónico",
|
||||
"password": "Contraseña",
|
||||
"submit": "Enviar",
|
||||
"error": "Credenciales inválidas"
|
||||
},
|
||||
"dashboard": {
|
||||
"welcome": "Bienvenido a tu panel",
|
||||
"stats": "Tus Estadísticas",
|
||||
"recentActivity": "Actividad Reciente"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Panel de Administración",
|
||||
"users": "Gestión de Usuarios",
|
||||
"settings": "Configuración del Sistema"
|
||||
},
|
||||
"home": {
|
||||
"welcome": "Bienvenido a la Página de Inicio"
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,45 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
.font-neue {
|
||||
font-family: "PP Neue Montreal TT", sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.w-110 {
|
||||
width: 27.5rem;
|
||||
}
|
||||
.h-7\.5 {
|
||||
height: 1.1875rem;
|
||||
}
|
||||
.h-18 {
|
||||
height: 4.5rem;
|
||||
}
|
||||
.h-238 {
|
||||
height: 14.875rem;
|
||||
}
|
||||
.top-158 {
|
||||
top: 9.875rem;
|
||||
}
|
||||
.top-254 {
|
||||
top: 15.875rem;
|
||||
}
|
||||
.top-284 {
|
||||
top: 17.75rem;
|
||||
}
|
||||
.top-360 {
|
||||
top: 22.5rem;
|
||||
}
|
||||
.left-297 {
|
||||
left: 18.5625rem;
|
||||
}
|
||||
.left-34 {
|
||||
left: 2.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
|
||||
3
autogpt_platform/frontend/src/app/health/page.tsx
Normal file
3
autogpt_platform/frontend/src/app/health/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function HealthPage() {
|
||||
return <div>Yay im healthy</div>;
|
||||
}
|
||||
@@ -2,13 +2,15 @@ import React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import { Providers } from "@/app/providers";
|
||||
import { NavBar } from "@/components/NavBar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Navbar } from "@/components/agptui/Navbar";
|
||||
|
||||
import "./globals.css";
|
||||
import TallyPopupSimple from "@/components/TallyPopup";
|
||||
import { GoogleAnalytics } from "@next/third-parties/google";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { IconType } from "@/components/ui/icons";
|
||||
import { createServerClient } from "@/lib/supabase/server";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
@@ -17,23 +19,88 @@ export const metadata: Metadata = {
|
||||
description: "Your one stop shop to creating AI Agents",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const supabase = createServerClient();
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={cn("antialiased transition-colors", inter.className)}>
|
||||
<Providers
|
||||
initialUser={user}
|
||||
attribute="class"
|
||||
defaultTheme="light"
|
||||
// Feel free to remove this line if you want to use the system theme by default
|
||||
// enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<NavBar />
|
||||
<div className="flex min-h-screen flex-col items-center justify-center">
|
||||
<Navbar
|
||||
user={user}
|
||||
isLoggedIn={!!user}
|
||||
links={[
|
||||
{
|
||||
name: "Agent Store",
|
||||
href: "/store",
|
||||
},
|
||||
{
|
||||
name: "Library",
|
||||
href: "/monitoring",
|
||||
},
|
||||
{
|
||||
name: "Build",
|
||||
href: "/build",
|
||||
},
|
||||
]}
|
||||
menuItemGroups={[
|
||||
{
|
||||
items: [
|
||||
{
|
||||
icon: IconType.Edit,
|
||||
text: "Edit profile",
|
||||
href: "/store/profile",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{
|
||||
icon: IconType.LayoutDashboard,
|
||||
text: "Creator Dashboard",
|
||||
href: "/store/dashboard",
|
||||
},
|
||||
{
|
||||
icon: IconType.UploadCloud,
|
||||
text: "Publish an agent",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{
|
||||
icon: IconType.Settings,
|
||||
text: "Settings",
|
||||
href: "/store/settings",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{
|
||||
icon: IconType.LogOut,
|
||||
text: "Log out",
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<main className="flex-1 p-4">{children}</main>
|
||||
<TallyPopupSimple />
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,30 @@ const loginFormSchema = z.object({
|
||||
password: z.string().min(6).max(64),
|
||||
});
|
||||
|
||||
export async function logout() {
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"logout",
|
||||
{},
|
||||
async () => {
|
||||
const supabase = createServerClient();
|
||||
|
||||
if (!supabase) {
|
||||
redirect("/error");
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.signOut();
|
||||
|
||||
if (error) {
|
||||
console.log("Error logging out", error);
|
||||
return error.message;
|
||||
}
|
||||
|
||||
revalidatePath("/", "layout");
|
||||
redirect("/login");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function login(values: z.infer<typeof loginFormSchema>) {
|
||||
return await Sentry.withServerActionInstrumentation("login", {}, async () => {
|
||||
const supabase = createServerClient();
|
||||
@@ -22,9 +46,10 @@ export async function login(values: z.infer<typeof loginFormSchema>) {
|
||||
const { data, error } = await supabase.auth.signInWithPassword(values);
|
||||
|
||||
if (error) {
|
||||
console.log("Error logging in", error);
|
||||
if (error.status == 400) {
|
||||
// Hence User is not present
|
||||
redirect("/signup");
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
return error.message;
|
||||
@@ -33,8 +58,44 @@ export async function login(values: z.infer<typeof loginFormSchema>) {
|
||||
if (data.session) {
|
||||
await supabase.auth.setSession(data.session);
|
||||
}
|
||||
|
||||
console.log("Logged in");
|
||||
revalidatePath("/", "layout");
|
||||
redirect("/");
|
||||
});
|
||||
}
|
||||
|
||||
export async function signup(values: z.infer<typeof loginFormSchema>) {
|
||||
"use server";
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"signup",
|
||||
{},
|
||||
async () => {
|
||||
const supabase = createServerClient();
|
||||
|
||||
if (!supabase) {
|
||||
redirect("/error");
|
||||
}
|
||||
|
||||
// We are sure that the values are of the correct type because zod validates the form
|
||||
const { data, error } = await supabase.auth.signUp(values);
|
||||
|
||||
if (error) {
|
||||
console.log("Error signing up", error);
|
||||
if (error.message.includes("P0001")) {
|
||||
return "Please join our waitlist for your turn: https://agpt.co/waitlist";
|
||||
}
|
||||
if (error.code?.includes("user_already_exists")) {
|
||||
redirect("/login");
|
||||
}
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (data.session) {
|
||||
await supabase.auth.setSession(data.session);
|
||||
}
|
||||
console.log("Signed up");
|
||||
revalidatePath("/", "layout");
|
||||
redirect("/store/profile");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PasswordInput } from "@/components/PasswordInput";
|
||||
import { FaGoogle, FaGithub, FaDiscord, FaSpinner } from "react-icons/fa";
|
||||
import { useState } from "react";
|
||||
import { useSupabase } from "@/components/SupabaseProvider";
|
||||
import { useSupabase } from "@/components/providers/SupabaseProvider";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
@@ -203,14 +203,34 @@ export default function LoginPage() {
|
||||
className="flex w-full justify-center"
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
onClick={async () => {
|
||||
setIsLoading(true);
|
||||
const values = form.getValues();
|
||||
const result = await login(values);
|
||||
if (result) {
|
||||
setFeedback(result);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}}
|
||||
>
|
||||
Log in
|
||||
{isLoading ? <FaSpinner className="animate-spin" /> : "Log in"}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex w-full justify-center"
|
||||
type="button"
|
||||
disabled={isLoading}
|
||||
onClick={async () => {
|
||||
setIsLoading(true);
|
||||
const values = form.getValues();
|
||||
const result = await signup(values);
|
||||
if (result) {
|
||||
setFeedback(result);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}}
|
||||
>
|
||||
{isLoading ? <FaSpinner className="animate-spin" /> : "Sign up"}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full text-center">
|
||||
<Link href={"/signup"} className="w-fit text-xs hover:underline">
|
||||
Create a new Account
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
<p className="text-sm text-red-500">{feedback}</p>
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Suspense } from "react";
|
||||
import { notFound } from "next/navigation";
|
||||
import MarketplaceAPI from "@/lib/marketplace-api";
|
||||
import { AgentDetailResponse } from "@/lib/marketplace-api";
|
||||
import AgentDetailContent from "@/components/marketplace/AgentDetailContent";
|
||||
|
||||
async function getAgentDetails(id: string): Promise<AgentDetailResponse> {
|
||||
const apiUrl =
|
||||
process.env.NEXT_PUBLIC_AGPT_MARKETPLACE_URL ||
|
||||
"http://localhost:8015/api/v1/market";
|
||||
const api = new MarketplaceAPI(apiUrl);
|
||||
try {
|
||||
console.log(`Fetching agent details for id: ${id}`);
|
||||
const agent = await api.getAgentDetails(id);
|
||||
console.log(`Agent details fetched:`, agent);
|
||||
return agent;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching agent details:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function AgentDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: { id: string };
|
||||
}) {
|
||||
let agent: AgentDetailResponse;
|
||||
|
||||
try {
|
||||
agent = await getAgentDetails(params.id);
|
||||
} catch (error) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<AgentDetailContent agent={agent} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -1,352 +0,0 @@
|
||||
"use client";
|
||||
import React, { useEffect, useMemo, useState, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Image from "next/image";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import MarketplaceAPI, {
|
||||
AgentResponse,
|
||||
AgentWithRank,
|
||||
} from "@/lib/marketplace-api";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
PlusCircle,
|
||||
Search,
|
||||
Star,
|
||||
} from "lucide-react";
|
||||
|
||||
// Utility Functions
|
||||
function debounce<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number,
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
return (...args: Parameters<T>) => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func(...args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
// Types
|
||||
type Agent = AgentResponse | AgentWithRank;
|
||||
|
||||
// Components
|
||||
const HeroSection: React.FC = () => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="relative bg-indigo-600 py-6">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="https://images.unsplash.com/photo-1562408590-e32931084e23?auto=format&fit=crop&w=2070&q=80"
|
||||
alt="Marketplace background"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
quality={75}
|
||||
priority
|
||||
className="opacity-20"
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 bg-indigo-600 mix-blend-multiply"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
</div>
|
||||
<div className="relative mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6 lg:px-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-extrabold tracking-tight text-white sm:text-3xl lg:text-4xl">
|
||||
AutoGPT Marketplace
|
||||
</h1>
|
||||
<p className="mt-2 max-w-3xl text-sm text-indigo-100 sm:text-base">
|
||||
Discover and share proven AI Agents to supercharge your business.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => router.push("/marketplace/submit")}
|
||||
className="flex items-center bg-white text-indigo-600 hover:bg-indigo-50"
|
||||
>
|
||||
<PlusCircle className="mr-2 h-4 w-4" />
|
||||
Submit Agent
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SearchInput: React.FC<{
|
||||
value: string;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}> = ({ value, onChange }) => (
|
||||
<div className="relative mb-8">
|
||||
<Input
|
||||
placeholder="Search agents..."
|
||||
type="text"
|
||||
className="w-full rounded-full border-gray-300 py-2 pl-10 pr-4 focus:border-indigo-500 focus:ring-indigo-500"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<Search
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 transform text-gray-400"
|
||||
size={20}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const AgentCard: React.FC<{ agent: Agent; featured?: boolean }> = ({
|
||||
agent,
|
||||
featured = false,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
const handleClick = () => {
|
||||
router.push(`/marketplace/${agent.id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex cursor-pointer flex-col justify-between rounded-lg border p-6 transition-colors duration-200 hover:bg-gray-50 ${featured ? "border-indigo-500 shadow-md" : "border-gray-300"}`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h3 className="truncate text-lg font-semibold text-gray-900">
|
||||
{agent.name}
|
||||
</h3>
|
||||
{featured && <Star className="text-indigo-500" size={20} />}
|
||||
</div>
|
||||
<p className="mb-4 line-clamp-2 text-sm text-gray-500">
|
||||
{agent.description}
|
||||
</p>
|
||||
<div className="mb-2 text-xs text-gray-400">
|
||||
Categories: {agent.categories?.join(", ")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end justify-between">
|
||||
<div className="text-xs text-gray-400">
|
||||
Updated {new Date(agent.updatedAt).toLocaleDateString()}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">Downloads {agent.downloads}</div>
|
||||
{"rank" in agent && (
|
||||
<div className="text-xs text-indigo-600">
|
||||
Rank: {agent.rank.toFixed(2)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AgentGrid: React.FC<{
|
||||
agents: Agent[];
|
||||
title: string;
|
||||
featured?: boolean;
|
||||
}> = ({ agents, title, featured = false }) => (
|
||||
<div className="mb-12">
|
||||
<h2 className="mb-4 text-2xl font-bold text-gray-900">{title}</h2>
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{agents.map((agent) => (
|
||||
<AgentCard agent={agent} key={agent.id} featured={featured} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Pagination: React.FC<{
|
||||
page: number;
|
||||
totalPages: number;
|
||||
onPrevPage: () => void;
|
||||
onNextPage: () => void;
|
||||
}> = ({ page, totalPages, onPrevPage, onNextPage }) => (
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<Button
|
||||
onClick={onPrevPage}
|
||||
disabled={page === 1}
|
||||
className="flex items-center space-x-2 rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
<span>Previous</span>
|
||||
</Button>
|
||||
<span className="text-sm text-gray-700">
|
||||
Page {page} of {totalPages}
|
||||
</span>
|
||||
<Button
|
||||
onClick={onNextPage}
|
||||
disabled={page === totalPages}
|
||||
className="flex items-center space-x-2 rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50"
|
||||
>
|
||||
<span>Next</span>
|
||||
<ChevronRight size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Main Component
|
||||
const Marketplace: React.FC = () => {
|
||||
const apiUrl =
|
||||
process.env.NEXT_PUBLIC_AGPT_MARKETPLACE_URL ||
|
||||
"http://localhost:8015/api/v1/market";
|
||||
const api = useMemo(() => new MarketplaceAPI(apiUrl), [apiUrl]);
|
||||
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [searchResults, setSearchResults] = useState<Agent[]>([]);
|
||||
const [featuredAgents, setFeaturedAgents] = useState<Agent[]>([]);
|
||||
const [topAgents, setTopAgents] = useState<Agent[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [topAgentsPage, setTopAgentsPage] = useState(1);
|
||||
const [searchPage, setSearchPage] = useState(1);
|
||||
const [topAgentsTotalPages, setTopAgentsTotalPages] = useState(1);
|
||||
const [searchTotalPages, setSearchTotalPages] = useState(1);
|
||||
|
||||
const fetchTopAgents = useCallback(
|
||||
async (currentPage: number) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await api.getTopDownloadedAgents(currentPage, 9);
|
||||
setTopAgents(response.items);
|
||||
setTopAgentsTotalPages(response.total_pages);
|
||||
} catch (error) {
|
||||
console.error("Error fetching top agents:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[api],
|
||||
);
|
||||
|
||||
const fetchFeaturedAgents = useCallback(async () => {
|
||||
try {
|
||||
const featured = await api.getFeaturedAgents();
|
||||
setFeaturedAgents(featured.items);
|
||||
} catch (error) {
|
||||
console.error("Error fetching featured agents:", error);
|
||||
}
|
||||
}, [api]);
|
||||
|
||||
const searchAgents = useCallback(
|
||||
async (searchTerm: string, currentPage: number) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await api.searchAgents(searchTerm, currentPage, 9);
|
||||
const filteredAgents = response.items.filter((agent) => agent.rank > 0);
|
||||
setSearchResults(filteredAgents);
|
||||
setSearchTotalPages(response.total_pages);
|
||||
} catch (error) {
|
||||
console.error("Error searching agents:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[api],
|
||||
);
|
||||
|
||||
const debouncedSearch = useMemo(
|
||||
() => debounce(searchAgents, 300),
|
||||
[searchAgents],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchValue) {
|
||||
searchAgents(searchValue, searchPage);
|
||||
} else {
|
||||
fetchTopAgents(topAgentsPage);
|
||||
}
|
||||
}, [searchValue, searchPage, topAgentsPage, searchAgents, fetchTopAgents]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFeaturedAgents();
|
||||
}, [fetchFeaturedAgents]);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchValue(e.target.value);
|
||||
setSearchPage(1);
|
||||
};
|
||||
|
||||
const handleNextPage = () => {
|
||||
if (searchValue) {
|
||||
if (searchPage < searchTotalPages) {
|
||||
setSearchPage(searchPage + 1);
|
||||
}
|
||||
} else {
|
||||
if (topAgentsPage < topAgentsTotalPages) {
|
||||
setTopAgentsPage(topAgentsPage + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevPage = () => {
|
||||
if (searchValue) {
|
||||
if (searchPage > 1) {
|
||||
setSearchPage(searchPage - 1);
|
||||
}
|
||||
} else {
|
||||
if (topAgentsPage > 1) {
|
||||
setTopAgentsPage(topAgentsPage - 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<HeroSection />
|
||||
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
|
||||
<SearchInput value={searchValue} onChange={handleInputChange} />
|
||||
{isLoading ? (
|
||||
<div className="py-12 text-center">
|
||||
<div className="inline-block h-8 w-8 animate-spin rounded-full border-b-2 border-gray-900"></div>
|
||||
<p className="mt-2 text-gray-600">Loading agents...</p>
|
||||
</div>
|
||||
) : searchValue ? (
|
||||
searchResults.length > 0 ? (
|
||||
<>
|
||||
<AgentGrid agents={searchResults} title="Search Results" />
|
||||
<Pagination
|
||||
page={searchPage}
|
||||
totalPages={searchTotalPages}
|
||||
onPrevPage={handlePrevPage}
|
||||
onNextPage={handleNextPage}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="py-12 text-center">
|
||||
<p className="text-gray-600">
|
||||
No agents found matching your search criteria.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{featuredAgents?.length > 0 ? (
|
||||
<AgentGrid
|
||||
agents={featuredAgents}
|
||||
title="Featured Agents"
|
||||
featured={true}
|
||||
/>
|
||||
) : (
|
||||
<div className="py-12 text-center">
|
||||
<p className="text-gray-600">No Featured Agents found</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<hr />
|
||||
|
||||
{topAgents?.length > 0 ? (
|
||||
<AgentGrid agents={topAgents} title="Top Downloaded Agents" />
|
||||
) : (
|
||||
<div className="py-12 text-center">
|
||||
<p className="text-gray-600">No Top Downloaded Agents found</p>
|
||||
</div>
|
||||
)}
|
||||
<Pagination
|
||||
page={topAgentsPage}
|
||||
totalPages={topAgentsTotalPages}
|
||||
onPrevPage={handlePrevPage}
|
||||
onNextPage={handleNextPage}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Marketplace;
|
||||
@@ -1,453 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import MarketplaceAPI from "@/lib/marketplace-api";
|
||||
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
MultiSelector,
|
||||
MultiSelectorContent,
|
||||
MultiSelectorInput,
|
||||
MultiSelectorItem,
|
||||
MultiSelectorList,
|
||||
MultiSelectorTrigger,
|
||||
} from "@/components/ui/multiselect";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
type FormData = {
|
||||
name: string;
|
||||
description: string;
|
||||
author: string;
|
||||
keywords: string[];
|
||||
categories: string[];
|
||||
agreeToTerms: boolean;
|
||||
selectedAgentId: string;
|
||||
};
|
||||
|
||||
const keywords = [
|
||||
"Automation",
|
||||
"AI Workflows",
|
||||
"Integration",
|
||||
"Task Automation",
|
||||
"Data Processing",
|
||||
"Workflow Management",
|
||||
"Real-time Analytics",
|
||||
"Custom Triggers",
|
||||
"Event-driven",
|
||||
"API Integration",
|
||||
"Data Transformation",
|
||||
"Multi-step Workflows",
|
||||
"Collaboration Tools",
|
||||
"Business Process Automation",
|
||||
"No-code Solutions",
|
||||
"AI-Powered",
|
||||
"Smart Notifications",
|
||||
"Data Syncing",
|
||||
"User Engagement",
|
||||
"Reporting Automation",
|
||||
"Lead Generation",
|
||||
"Customer Support Automation",
|
||||
"E-commerce Automation",
|
||||
"Social Media Management",
|
||||
"Email Marketing Automation",
|
||||
"Document Management",
|
||||
"Data Enrichment",
|
||||
"Performance Tracking",
|
||||
"Predictive Analytics",
|
||||
"Resource Allocation",
|
||||
"Chatbot",
|
||||
"Virtual Assistant",
|
||||
"Workflow Automation",
|
||||
"Social Media Manager",
|
||||
"Email Optimizer",
|
||||
"Content Generator",
|
||||
"Data Analyzer",
|
||||
"Task Scheduler",
|
||||
"Customer Service Bot",
|
||||
"Personalization Engine",
|
||||
];
|
||||
|
||||
const SubmitPage: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<FormData>({
|
||||
defaultValues: {
|
||||
selectedAgentId: "", // Initialize with an empty string
|
||||
name: "",
|
||||
description: "",
|
||||
author: "",
|
||||
keywords: [],
|
||||
categories: [],
|
||||
agreeToTerms: false,
|
||||
},
|
||||
});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
const [userAgents, setUserAgents] = useState<
|
||||
Array<{ id: string; name: string; version: number }>
|
||||
>([]);
|
||||
const [selectedAgentGraph, setSelectedAgentGraph] = useState<any>(null);
|
||||
|
||||
const selectedAgentId = watch("selectedAgentId");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserAgents = async () => {
|
||||
const api = new AutoGPTServerAPI();
|
||||
const agents = await api.listGraphs();
|
||||
console.log(agents);
|
||||
setUserAgents(
|
||||
agents.map((agent) => ({
|
||||
id: agent.id,
|
||||
name: agent.name || `Agent (${agent.id})`,
|
||||
version: agent.version,
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
fetchUserAgents();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAgentGraph = async () => {
|
||||
if (selectedAgentId) {
|
||||
const api = new AutoGPTServerAPI();
|
||||
const graph = await api.getGraph(selectedAgentId, undefined, true);
|
||||
setSelectedAgentGraph(graph);
|
||||
setValue("name", graph.name);
|
||||
setValue("description", graph.description);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAgentGraph();
|
||||
}, [selectedAgentId, setValue]);
|
||||
|
||||
const onSubmit = async (data: FormData) => {
|
||||
setIsSubmitting(true);
|
||||
setSubmitError(null);
|
||||
|
||||
if (!data.agreeToTerms) {
|
||||
throw new Error("You must agree to the terms of use");
|
||||
}
|
||||
|
||||
try {
|
||||
if (!selectedAgentGraph) {
|
||||
throw new Error("Please select an agent");
|
||||
}
|
||||
|
||||
const api = new MarketplaceAPI();
|
||||
await api.submitAgent(
|
||||
{
|
||||
...selectedAgentGraph,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
},
|
||||
data.author,
|
||||
data.keywords,
|
||||
data.categories,
|
||||
);
|
||||
|
||||
router.push("/marketplace?submission=success");
|
||||
} catch (error) {
|
||||
console.error("Submission error:", error);
|
||||
setSubmitError(
|
||||
error instanceof Error ? error.message : "An unknown error occurred",
|
||||
);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<h1 className="mb-6 text-3xl font-bold">Submit Your Agent</h1>
|
||||
<Card className="p-6">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-4">
|
||||
<Controller
|
||||
name="selectedAgentId"
|
||||
control={control}
|
||||
rules={{ required: "Please select an agent" }}
|
||||
render={({ field }) => (
|
||||
<div>
|
||||
<label
|
||||
htmlFor={field.name}
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Select Agent
|
||||
</label>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value || ""}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select an agent" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{userAgents.map((agent) => (
|
||||
<SelectItem key={agent.id} value={agent.id}>
|
||||
{agent.name} (v{agent.version})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.selectedAgentId && (
|
||||
<p className="mt-1 text-sm text-red-600">
|
||||
{errors.selectedAgentId.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* {selectedAgentGraph && (
|
||||
<div className="mt-4" style={{ height: "600px" }}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
fitView
|
||||
attributionPosition="bottom-left"
|
||||
nodesConnectable={false}
|
||||
nodesDraggable={false}
|
||||
zoomOnScroll={false}
|
||||
panOnScroll={false}
|
||||
elementsSelectable={false}
|
||||
>
|
||||
<Controls showInteractive={false} />
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
rules={{ required: "Name is required" }}
|
||||
render={({ field }) => (
|
||||
<div>
|
||||
<label
|
||||
htmlFor={field.name}
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Agent Name
|
||||
</label>
|
||||
<Input
|
||||
id={field.name}
|
||||
placeholder="Enter your agent's name"
|
||||
{...field}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="mt-1 text-sm text-red-600">
|
||||
{errors.name.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="description"
|
||||
control={control}
|
||||
rules={{ required: "Description is required" }}
|
||||
render={({ field }) => (
|
||||
<div>
|
||||
<label
|
||||
htmlFor={field.name}
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<Textarea
|
||||
id={field.name}
|
||||
placeholder="Describe your agent"
|
||||
{...field}
|
||||
/>
|
||||
{errors.description && (
|
||||
<p className="mt-1 text-sm text-red-600">
|
||||
{errors.description.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="author"
|
||||
control={control}
|
||||
rules={{ required: "Author is required" }}
|
||||
render={({ field }) => (
|
||||
<div>
|
||||
<label
|
||||
htmlFor={field.name}
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Author
|
||||
</label>
|
||||
<Input
|
||||
id={field.name}
|
||||
placeholder="Your name or username"
|
||||
{...field}
|
||||
/>
|
||||
{errors.author && (
|
||||
<p className="mt-1 text-sm text-red-600">
|
||||
{errors.author.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="keywords"
|
||||
control={control}
|
||||
rules={{ required: "At least one keyword is required" }}
|
||||
render={({ field }) => (
|
||||
<div>
|
||||
<label
|
||||
htmlFor={field.name}
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Keywords
|
||||
</label>
|
||||
<MultiSelector
|
||||
values={field.value || []}
|
||||
onValuesChange={field.onChange}
|
||||
>
|
||||
<MultiSelectorTrigger>
|
||||
<MultiSelectorInput placeholder="Add keywords" />
|
||||
</MultiSelectorTrigger>
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
{keywords.map((keyword) => (
|
||||
<MultiSelectorItem key={keyword} value={keyword}>
|
||||
{keyword}
|
||||
</MultiSelectorItem>
|
||||
))}
|
||||
{/* Add more predefined keywords as needed */}
|
||||
</MultiSelectorList>
|
||||
</MultiSelectorContent>
|
||||
</MultiSelector>
|
||||
{errors.keywords && (
|
||||
<p className="mt-1 text-sm text-red-600">
|
||||
{errors.keywords.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="categories"
|
||||
control={control}
|
||||
rules={{ required: "At least one category is required" }}
|
||||
render={({ field }) => (
|
||||
<div>
|
||||
<label
|
||||
htmlFor={field.name}
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
Categories
|
||||
</label>
|
||||
<MultiSelector
|
||||
values={field.value || []}
|
||||
onValuesChange={field.onChange}
|
||||
>
|
||||
<MultiSelectorTrigger>
|
||||
<MultiSelectorInput placeholder="Select categories" />
|
||||
</MultiSelectorTrigger>
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
<MultiSelectorItem value="productivity">
|
||||
Productivity
|
||||
</MultiSelectorItem>
|
||||
<MultiSelectorItem value="entertainment">
|
||||
Entertainment
|
||||
</MultiSelectorItem>
|
||||
<MultiSelectorItem value="education">
|
||||
Education
|
||||
</MultiSelectorItem>
|
||||
<MultiSelectorItem value="business">
|
||||
Business
|
||||
</MultiSelectorItem>
|
||||
<MultiSelectorItem value="other">
|
||||
Other
|
||||
</MultiSelectorItem>
|
||||
</MultiSelectorList>
|
||||
</MultiSelectorContent>
|
||||
</MultiSelector>
|
||||
{errors.categories && (
|
||||
<p className="mt-1 text-sm text-red-600">
|
||||
{errors.categories.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="agreeToTerms"
|
||||
control={control}
|
||||
rules={{ required: "You must agree to the terms of use" }}
|
||||
render={({ field }) => (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="agreeToTerms"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
<label
|
||||
htmlFor="agreeToTerms"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
I agree to the{" "}
|
||||
<a
|
||||
href="https://auto-gpt.notion.site/Terms-of-Use-11400ef5bece80d0b087d7831c5fd6bf"
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
terms of use
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{errors.agreeToTerms && (
|
||||
<p className="mt-1 text-sm text-red-600">
|
||||
{errors.agreeToTerms.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{submitError && (
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Submission Failed</AlertTitle>
|
||||
<AlertDescription>{submitError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
||||
{isSubmitting ? "Submitting..." : "Submit Agent"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubmitPage;
|
||||
168
autogpt_platform/frontend/src/app/monitoring/page.tsx
Normal file
168
autogpt_platform/frontend/src/app/monitoring/page.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||
|
||||
import AutoGPTServerAPI, {
|
||||
GraphMetaWithRuns,
|
||||
ExecutionMeta,
|
||||
Schedule,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { FlowRun } from "@/lib/types";
|
||||
import {
|
||||
AgentFlowList,
|
||||
FlowInfo,
|
||||
FlowRunInfo,
|
||||
FlowRunsList,
|
||||
FlowRunsStats,
|
||||
} from "@/components/monitor";
|
||||
import { SchedulesTable } from "@/components/monitor/scheduleTable";
|
||||
|
||||
const Monitor = () => {
|
||||
const [flows, setFlows] = useState<GraphMetaWithRuns[]>([]);
|
||||
const [flowRuns, setFlowRuns] = useState<FlowRun[]>([]);
|
||||
const [schedules, setSchedules] = useState<Schedule[]>([]);
|
||||
const [selectedFlow, setSelectedFlow] = useState<GraphMetaWithRuns | null>(
|
||||
null,
|
||||
);
|
||||
const [selectedRun, setSelectedRun] = useState<FlowRun | null>(null);
|
||||
const [sortColumn, setSortColumn] = useState<keyof Schedule>("id");
|
||||
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
|
||||
|
||||
const api = useMemo(() => new AutoGPTServerAPI(), []);
|
||||
|
||||
const fetchSchedules = useCallback(async () => {
|
||||
setSchedules(await api.listSchedules());
|
||||
}, [api]);
|
||||
|
||||
const removeSchedule = useCallback(
|
||||
async (scheduleId: string) => {
|
||||
const removedSchedule = await api.deleteSchedule(scheduleId);
|
||||
setSchedules(schedules.filter((s) => s.id !== removedSchedule.id));
|
||||
},
|
||||
[schedules, api],
|
||||
);
|
||||
|
||||
const fetchAgents = useCallback(() => {
|
||||
api.listGraphsWithRuns().then((agent) => {
|
||||
setFlows(agent);
|
||||
const flowRuns = agent.flatMap((graph) =>
|
||||
graph.executions != null
|
||||
? graph.executions.map((execution) =>
|
||||
flowRunFromExecutionMeta(graph, execution),
|
||||
)
|
||||
: [],
|
||||
);
|
||||
setFlowRuns(flowRuns);
|
||||
});
|
||||
}, [api]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAgents();
|
||||
}, [fetchAgents]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSchedules();
|
||||
}, [fetchSchedules]);
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(() => fetchAgents(), 5000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [fetchAgents, flows]);
|
||||
|
||||
const column1 = "md:col-span-2 xl:col-span-3 xxl:col-span-2";
|
||||
const column2 = "md:col-span-3 lg:col-span-2 xl:col-span-3";
|
||||
const column3 = "col-span-full xl:col-span-4 xxl:col-span-5";
|
||||
|
||||
const handleSort = (column: keyof Schedule) => {
|
||||
if (sortColumn === column) {
|
||||
setSortDirection(sortDirection === "asc" ? "desc" : "asc");
|
||||
} else {
|
||||
setSortColumn(column);
|
||||
setSortDirection("asc");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="grid h-full w-screen grid-cols-1 gap-4 px-8 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10"
|
||||
data-testid="monitor-page"
|
||||
>
|
||||
<AgentFlowList
|
||||
className={column1}
|
||||
flows={flows}
|
||||
flowRuns={flowRuns}
|
||||
selectedFlow={selectedFlow}
|
||||
onSelectFlow={(f) => {
|
||||
setSelectedRun(null);
|
||||
setSelectedFlow(
|
||||
f.id == selectedFlow?.id ? null : (f as GraphMetaWithRuns),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FlowRunsList
|
||||
className={column2}
|
||||
flows={flows}
|
||||
runs={[
|
||||
...(selectedFlow
|
||||
? flowRuns.filter((v) => v.graphID == selectedFlow.id)
|
||||
: flowRuns),
|
||||
].sort((a, b) => Number(a.startTime) - Number(b.startTime))}
|
||||
selectedRun={selectedRun}
|
||||
onSelectRun={(r) => setSelectedRun(r.id == selectedRun?.id ? null : r)}
|
||||
/>
|
||||
{(selectedRun && (
|
||||
<FlowRunInfo
|
||||
flow={selectedFlow || flows.find((f) => f.id == selectedRun.graphID)!}
|
||||
flowRun={selectedRun}
|
||||
className={column3}
|
||||
/>
|
||||
)) ||
|
||||
(selectedFlow && (
|
||||
<FlowInfo
|
||||
flow={selectedFlow}
|
||||
flowRuns={flowRuns.filter((r) => r.graphID == selectedFlow.id)}
|
||||
className={column3}
|
||||
refresh={() => {
|
||||
fetchAgents();
|
||||
setSelectedFlow(null);
|
||||
setSelectedRun(null);
|
||||
}}
|
||||
/>
|
||||
)) || (
|
||||
<Card className={`p-6 ${column3}`}>
|
||||
<FlowRunsStats flows={flows} flowRuns={flowRuns} />
|
||||
</Card>
|
||||
)}
|
||||
<div className="col-span-full xl:col-span-6">
|
||||
<SchedulesTable
|
||||
schedules={schedules} // all schedules
|
||||
agents={flows} // for filtering purpose
|
||||
onRemoveSchedule={removeSchedule}
|
||||
sortColumn={sortColumn}
|
||||
sortDirection={sortDirection}
|
||||
onSort={handleSort}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function flowRunFromExecutionMeta(
|
||||
graphMeta: GraphMetaWithRuns,
|
||||
executionMeta: ExecutionMeta,
|
||||
): FlowRun {
|
||||
return {
|
||||
id: executionMeta.execution_id,
|
||||
graphID: graphMeta.id,
|
||||
graphVersion: graphMeta.version,
|
||||
status: executionMeta.status,
|
||||
startTime: executionMeta.started_at,
|
||||
endTime: executionMeta.ended_at,
|
||||
duration: executionMeta.duration,
|
||||
totalRunTime: executionMeta.total_run_time,
|
||||
} as FlowRun;
|
||||
}
|
||||
|
||||
export default Monitor;
|
||||
@@ -1,145 +1,7 @@
|
||||
"use client";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import AutoGPTServerAPI, {
|
||||
GraphExecution,
|
||||
Schedule,
|
||||
GraphMeta,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
AgentFlowList,
|
||||
FlowInfo,
|
||||
FlowRunInfo,
|
||||
FlowRunsList,
|
||||
FlowRunsStats,
|
||||
} from "@/components/monitor";
|
||||
import { SchedulesTable } from "@/components/monitor/scheduleTable";
|
||||
|
||||
const Monitor = () => {
|
||||
const [flows, setFlows] = useState<GraphMeta[]>([]);
|
||||
const [executions, setExecutions] = useState<GraphExecution[]>([]);
|
||||
const [schedules, setSchedules] = useState<Schedule[]>([]);
|
||||
const [selectedFlow, setSelectedFlow] = useState<GraphMeta | null>(null);
|
||||
const [selectedRun, setSelectedRun] = useState<GraphExecution | null>(null);
|
||||
const [sortColumn, setSortColumn] = useState<keyof Schedule>("id");
|
||||
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
|
||||
|
||||
const api = useMemo(() => new AutoGPTServerAPI(), []);
|
||||
|
||||
const fetchSchedules = useCallback(async () => {
|
||||
setSchedules(await api.listSchedules());
|
||||
}, [api]);
|
||||
|
||||
const removeSchedule = useCallback(
|
||||
async (scheduleId: string) => {
|
||||
const removedSchedule = await api.deleteSchedule(scheduleId);
|
||||
setSchedules(schedules.filter((s) => s.id !== removedSchedule.id));
|
||||
},
|
||||
[schedules, api],
|
||||
);
|
||||
|
||||
const fetchAgents = useCallback(() => {
|
||||
api.listGraphs().then((agent) => {
|
||||
setFlows(agent);
|
||||
});
|
||||
api.getExecutions().then((executions) => {
|
||||
setExecutions(executions);
|
||||
});
|
||||
}, [api]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAgents();
|
||||
}, [fetchAgents]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSchedules();
|
||||
}, [fetchSchedules]);
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(() => fetchAgents(), 5000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [fetchAgents, flows]);
|
||||
|
||||
const column1 = "md:col-span-2 xl:col-span-3 xxl:col-span-2";
|
||||
const column2 = "md:col-span-3 lg:col-span-2 xl:col-span-3";
|
||||
const column3 = "col-span-full xl:col-span-4 xxl:col-span-5";
|
||||
|
||||
const handleSort = (column: keyof Schedule) => {
|
||||
if (sortColumn === column) {
|
||||
setSortDirection(sortDirection === "asc" ? "desc" : "asc");
|
||||
} else {
|
||||
setSortColumn(column);
|
||||
setSortDirection("asc");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="grid grid-cols-1 gap-4 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10"
|
||||
data-testid="monitor-page"
|
||||
>
|
||||
<AgentFlowList
|
||||
className={column1}
|
||||
flows={flows}
|
||||
executions={executions}
|
||||
selectedFlow={selectedFlow}
|
||||
onSelectFlow={(f) => {
|
||||
setSelectedRun(null);
|
||||
setSelectedFlow(f.id == selectedFlow?.id ? null : (f as GraphMeta));
|
||||
}}
|
||||
/>
|
||||
<FlowRunsList
|
||||
className={column2}
|
||||
flows={flows}
|
||||
executions={[
|
||||
...(selectedFlow
|
||||
? executions.filter((v) => v.graph_id == selectedFlow.id)
|
||||
: executions),
|
||||
].sort((a, b) => Number(b.started_at) - Number(a.started_at))}
|
||||
selectedRun={selectedRun}
|
||||
onSelectRun={(r) =>
|
||||
setSelectedRun(r.execution_id == selectedRun?.execution_id ? null : r)
|
||||
}
|
||||
/>
|
||||
{(selectedRun && (
|
||||
<FlowRunInfo
|
||||
flow={
|
||||
selectedFlow || flows.find((f) => f.id == selectedRun.graph_id)!
|
||||
}
|
||||
execution={selectedRun}
|
||||
className={column3}
|
||||
/>
|
||||
)) ||
|
||||
(selectedFlow && (
|
||||
<FlowInfo
|
||||
flow={selectedFlow}
|
||||
executions={executions.filter((e) => e.graph_id == selectedFlow.id)}
|
||||
className={column3}
|
||||
refresh={() => {
|
||||
fetchAgents();
|
||||
setSelectedFlow(null);
|
||||
setSelectedRun(null);
|
||||
}}
|
||||
/>
|
||||
)) || (
|
||||
<Card className={`p-6 ${column3}`}>
|
||||
<FlowRunsStats flows={flows} executions={executions} />
|
||||
</Card>
|
||||
)}
|
||||
<div className="col-span-full xl:col-span-6">
|
||||
<SchedulesTable
|
||||
schedules={schedules} // all schedules
|
||||
agents={flows} // for filtering purpose
|
||||
onRemoveSchedule={removeSchedule}
|
||||
sortColumn={sortColumn}
|
||||
sortDirection={sortDirection}
|
||||
onSort={handleSort}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Monitor;
|
||||
export default function Page() {
|
||||
redirect("/store");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useSupabase } from "@/components/SupabaseProvider";
|
||||
import { useSupabase } from "@/components/providers/SupabaseProvider";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import useUser from "@/hooks/useUser";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
@@ -2,17 +2,22 @@
|
||||
|
||||
import * as React from "react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { ThemeProviderProps } from "next-themes/dist/types";
|
||||
import { BackendAPIProvider } from "@/lib/autogpt-server-api";
|
||||
import { ThemeProviderProps } from "next-themes";
|
||||
import { BackendAPIProvider } from "@/lib/autogpt-server-api/context";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import SupabaseProvider from "@/components/SupabaseProvider";
|
||||
import SupabaseProvider from "@/components/providers/SupabaseProvider";
|
||||
import CredentialsProvider from "@/components/integrations/credentials-provider";
|
||||
import { User } from "@supabase/supabase-js";
|
||||
import { LaunchDarklyProvider } from "@/components/feature-flag/feature-flag-provider";
|
||||
|
||||
export function Providers({ children, ...props }: ThemeProviderProps) {
|
||||
export function Providers({
|
||||
children,
|
||||
initialUser,
|
||||
...props
|
||||
}: ThemeProviderProps & { initialUser: User | null }) {
|
||||
return (
|
||||
<NextThemesProvider {...props}>
|
||||
<SupabaseProvider>
|
||||
<SupabaseProvider initialUser={initialUser}>
|
||||
<BackendAPIProvider>
|
||||
<CredentialsProvider>
|
||||
<LaunchDarklyProvider>
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
"use server";
|
||||
import { createServerClient } from "@/lib/supabase/server";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
const SignupFormSchema = z.object({
|
||||
email: z.string().email().min(2).max(64),
|
||||
password: z.string().min(6).max(64),
|
||||
});
|
||||
|
||||
export async function signup(values: z.infer<typeof SignupFormSchema>) {
|
||||
"use server";
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"signup",
|
||||
{},
|
||||
async () => {
|
||||
const supabase = createServerClient();
|
||||
|
||||
if (!supabase) {
|
||||
redirect("/error");
|
||||
}
|
||||
|
||||
// We are sure that the values are of the correct type because zod validates the form
|
||||
const { data, error } = await supabase.auth.signUp(values);
|
||||
|
||||
if (error) {
|
||||
if (error.message.includes("P0001")) {
|
||||
return "Please join our waitlist for your turn: https://agpt.co/waitlist";
|
||||
}
|
||||
if (error.code?.includes("user_already_exists")) {
|
||||
redirect("/login");
|
||||
}
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (data.session) {
|
||||
await supabase.auth.setSession(data.session);
|
||||
}
|
||||
|
||||
revalidatePath("/", "layout");
|
||||
redirect("/");
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
"use client";
|
||||
import useUser from "@/hooks/useUser";
|
||||
import { signup } from "./actions";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PasswordInput } from "@/components/PasswordInput";
|
||||
import { FaGoogle, FaGithub, FaDiscord, FaSpinner } from "react-icons/fa";
|
||||
import { useState } from "react";
|
||||
import { useSupabase } from "@/components/SupabaseProvider";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
|
||||
const signupFormSchema = z.object({
|
||||
email: z.string().email().min(2).max(64),
|
||||
password: z.string().min(6).max(64),
|
||||
agreeToTerms: z.boolean().refine((value) => value === true, {
|
||||
message: "You must agree to the Terms of Use and Privacy Policy",
|
||||
}),
|
||||
});
|
||||
|
||||
export default function LoginPage() {
|
||||
const { supabase, isLoading: isSupabaseLoading } = useSupabase();
|
||||
const { user, isLoading: isUserLoading } = useUser();
|
||||
const [feedback, setFeedback] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const form = useForm<z.infer<typeof signupFormSchema>>({
|
||||
resolver: zodResolver(signupFormSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
agreeToTerms: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (user) {
|
||||
console.log("User exists, redirecting to home");
|
||||
router.push("/");
|
||||
}
|
||||
|
||||
if (isUserLoading || isSupabaseLoading || user) {
|
||||
return (
|
||||
<div className="flex h-[80vh] items-center justify-center">
|
||||
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!supabase) {
|
||||
return (
|
||||
<div>
|
||||
User accounts are disabled because Supabase client is unavailable
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSignInWithProvider(
|
||||
provider: "google" | "github" | "discord",
|
||||
) {
|
||||
const { data, error } = await supabase!.auth.signInWithOAuth({
|
||||
provider: provider,
|
||||
options: {
|
||||
redirectTo:
|
||||
process.env.AUTH_CALLBACK_URL ??
|
||||
`http://localhost:3000/auth/callback`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!error) {
|
||||
setFeedback(null);
|
||||
return;
|
||||
}
|
||||
setFeedback(error.message);
|
||||
}
|
||||
|
||||
const onSignup = async (data: z.infer<typeof signupFormSchema>) => {
|
||||
if (await form.trigger()) {
|
||||
setIsLoading(true);
|
||||
const error = await signup(data);
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
setFeedback(error);
|
||||
return;
|
||||
}
|
||||
setFeedback(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-[80vh] items-center justify-center">
|
||||
<div className="w-full max-w-md space-y-6 rounded-lg p-8 shadow-md">
|
||||
<h1 className="text-lg font-medium">Create a New Account</h1>
|
||||
{/* <div className="mb-6 space-y-2">
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => handleSignInWithProvider("google")}
|
||||
variant="outline"
|
||||
type="button"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<FaGoogle className="mr-2 h-4 w-4" />
|
||||
Sign in with Google
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => handleSignInWithProvider("github")}
|
||||
variant="outline"
|
||||
type="button"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<FaGithub className="mr-2 h-4 w-4" />
|
||||
Sign in with GitHub
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => handleSignInWithProvider("discord")}
|
||||
variant="outline"
|
||||
type="button"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<FaDiscord className="mr-2 h-4 w-4" />
|
||||
Sign in with Discord
|
||||
</Button>
|
||||
</div> */}
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSignup)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-4">
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="user@email.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput placeholder="password" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Password needs to be at least 6 characters long
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="agreeToTerms"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4 flex flex-row items-start space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>
|
||||
I agree to the{" "}
|
||||
<Link
|
||||
href="https://auto-gpt.notion.site/Terms-of-Use-11400ef5bece80d0b087d7831c5fd6bf"
|
||||
className="underline"
|
||||
>
|
||||
Terms of Use
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link
|
||||
href="https://www.notion.so/auto-gpt/Privacy-Policy-ab11c9c20dbd4de1a15dcffe84d77984"
|
||||
className="underline"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</FormLabel>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="mb-6 mt-8 flex w-full space-x-4">
|
||||
<Button
|
||||
className="flex w-full justify-center"
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={form.handleSubmit(onSignup)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Sign up
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full text-center">
|
||||
<Link href={"/login"} className="w-fit text-xs hover:underline">
|
||||
Already a member? Log In here
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
<p className="text-sm text-red-500">{feedback}</p>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { AgentTable } from "@/components/agptui/AgentTable";
|
||||
import { AgentTableRowProps } from "@/components/agptui/AgentTableRow";
|
||||
import { Button } from "@/components/agptui/Button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
|
||||
import { createClient } from "@/lib/supabase/client";
|
||||
import { StatusType } from "@/components/agptui/Status";
|
||||
import { PublishAgentPopout } from "@/components/agptui/composite/PublishAgentPopout";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
StoreSubmissionsResponse,
|
||||
StoreSubmissionRequest,
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
|
||||
async function getDashboardData() {
|
||||
const supabase = createClient();
|
||||
if (!supabase) {
|
||||
return { submissions: [] };
|
||||
}
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
|
||||
if (!session) {
|
||||
console.warn("--- No session found in profile page");
|
||||
return { profile: null };
|
||||
}
|
||||
|
||||
const api = new AutoGPTServerAPI(
|
||||
process.env.NEXT_PUBLIC_AGPT_SERVER_URL,
|
||||
process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL,
|
||||
supabase,
|
||||
);
|
||||
|
||||
try {
|
||||
const submissions = await api.getStoreSubmissions();
|
||||
return {
|
||||
submissions,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error fetching profile:", error);
|
||||
return {
|
||||
profile: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default function Page({}: {}) {
|
||||
const [submissions, setSubmissions] = useState<StoreSubmissionsResponse>();
|
||||
const [openPopout, setOpenPopout] = useState<boolean>(false);
|
||||
const [submissionData, setSubmissionData] =
|
||||
useState<StoreSubmissionRequest>();
|
||||
const [popoutStep, setPopoutStep] = useState<"select" | "info" | "review">(
|
||||
"info",
|
||||
);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const { submissions } = await getDashboardData();
|
||||
if (submissions) {
|
||||
setSubmissions(submissions as StoreSubmissionsResponse);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
const onEditSubmission = useCallback((submission: StoreSubmissionRequest) => {
|
||||
setSubmissionData(submission);
|
||||
setPopoutStep("review");
|
||||
setOpenPopout(true);
|
||||
}, []);
|
||||
|
||||
const onDeleteSubmission = useCallback(
|
||||
(submission_id: string) => {
|
||||
const supabase = createClient();
|
||||
if (!supabase) {
|
||||
return;
|
||||
}
|
||||
const api = new AutoGPTServerAPI(
|
||||
process.env.NEXT_PUBLIC_AGPT_SERVER_URL,
|
||||
process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL,
|
||||
supabase,
|
||||
);
|
||||
api.deleteStoreSubmission(submission_id);
|
||||
fetchData();
|
||||
},
|
||||
[fetchData],
|
||||
);
|
||||
|
||||
const onOpenPopout = useCallback(() => {
|
||||
setPopoutStep("select");
|
||||
setOpenPopout(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<main className="flex-1 py-8">
|
||||
{/* Header Section */}
|
||||
<div className="mb-8 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-4xl font-medium text-neutral-900 dark:text-neutral-100">
|
||||
Agent dashboard
|
||||
</h1>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-xl font-medium text-neutral-900 dark:text-neutral-100">
|
||||
Submit a New Agent
|
||||
</h2>
|
||||
<p className="text-sm text-[#707070] dark:text-neutral-400">
|
||||
Select from the list of agents you currently have, or upload from
|
||||
your local machine.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<PublishAgentPopout
|
||||
trigger={
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={onOpenPopout}
|
||||
className="h-9 rounded-full bg-black px-4 text-sm font-medium text-white hover:bg-neutral-700 dark:hover:bg-neutral-600"
|
||||
>
|
||||
Submit agent
|
||||
</Button>
|
||||
}
|
||||
openPopout={openPopout}
|
||||
inputStep={popoutStep}
|
||||
submissionData={submissionData}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator className="mb-8" />
|
||||
|
||||
{/* Agents Section */}
|
||||
<div>
|
||||
<h2 className="mb-4 text-xl font-bold text-neutral-900 dark:text-neutral-100">
|
||||
Your uploaded agents
|
||||
</h2>
|
||||
<AgentTable
|
||||
agents={
|
||||
(submissions?.submissions.map((submission, index) => ({
|
||||
id: index,
|
||||
agent_id: submission.agent_id,
|
||||
agent_version: submission.agent_version,
|
||||
sub_heading: submission.sub_heading,
|
||||
date_submitted: submission.date_submitted,
|
||||
agentName: submission.name,
|
||||
description: submission.description,
|
||||
imageSrc: submission.image_urls || [""],
|
||||
dateSubmitted: new Date(
|
||||
submission.date_submitted,
|
||||
).toLocaleDateString(),
|
||||
status: submission.status.toLowerCase() as StatusType,
|
||||
runs: submission.runs,
|
||||
rating: submission.rating,
|
||||
})) as AgentTableRowProps[]) || []
|
||||
}
|
||||
onEditSubmission={onEditSubmission}
|
||||
onDeleteSubmission={onDeleteSubmission}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
23
autogpt_platform/frontend/src/app/store/(user)/layout.tsx
Normal file
23
autogpt_platform/frontend/src/app/store/(user)/layout.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as React from "react";
|
||||
import { Sidebar } from "@/components/agptui/Sidebar";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const sidebarLinkGroups = [
|
||||
{
|
||||
links: [
|
||||
{ text: "Creator Dashboard", href: "/store/dashboard" },
|
||||
{ text: "Agent dashboard", href: "/store/agent-dashboard" },
|
||||
{ text: "Integrations", href: "/store/integrations" },
|
||||
{ text: "Profile", href: "/store/profile" },
|
||||
{ text: "Settings", href: "/store/settings" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen w-screen max-w-[1360px] flex-col lg:flex-row">
|
||||
<Sidebar linkGroups={sidebarLinkGroups} />
|
||||
<div className="pl-4">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as React from "react";
|
||||
import { ProfileInfoForm } from "@/components/agptui/ProfileInfoForm";
|
||||
import AutoGPTServerAPIServerSide from "@/lib/autogpt-server-api";
|
||||
import { createServerClient } from "@/lib/supabase/server";
|
||||
import { CreatorDetails } from "@/lib/autogpt-server-api/types";
|
||||
|
||||
async function getProfileData() {
|
||||
// Get the supabase client first
|
||||
const supabase = createServerClient();
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
|
||||
if (!session) {
|
||||
console.warn("--- No session found in profile page");
|
||||
return { profile: null };
|
||||
}
|
||||
|
||||
// Create API client with the same supabase instance
|
||||
const api = new AutoGPTServerAPIServerSide(
|
||||
process.env.NEXT_PUBLIC_AGPT_SERVER_URL,
|
||||
process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL,
|
||||
supabase, // Pass the supabase client instance
|
||||
);
|
||||
|
||||
try {
|
||||
const profile = await api.getStoreProfile("profile");
|
||||
return {
|
||||
profile,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error fetching profile:", error);
|
||||
return {
|
||||
profile: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default async function Page({}: {}) {
|
||||
const { profile } = await getProfileData();
|
||||
|
||||
if (!profile) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-4">
|
||||
<p>Please log in to view your profile</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center px-4">
|
||||
<ProfileInfoForm profile={profile as CreatorDetails} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { SettingsInputForm } from "@/components/agptui/SettingsInputForm";
|
||||
|
||||
export default function Page() {
|
||||
return <SettingsInputForm />;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
|
||||
import { BreadCrumbs } from "@/components/agptui/BreadCrumbs";
|
||||
import { AgentInfo } from "@/components/agptui/AgentInfo";
|
||||
import { AgentImages } from "@/components/agptui/AgentImages";
|
||||
import { AgentsSection } from "@/components/agptui/composite/AgentsSection";
|
||||
import { BecomeACreator } from "@/components/agptui/BecomeACreator";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { creator: string; slug: string };
|
||||
}): Promise<Metadata> {
|
||||
const api = new AutoGPTServerAPI();
|
||||
const agent = await api.getStoreAgent(params.creator, params.slug);
|
||||
|
||||
return {
|
||||
title: `${agent.agent_name} - AutoGPT Store`,
|
||||
description: agent.description,
|
||||
};
|
||||
}
|
||||
|
||||
// export async function generateStaticParams() {
|
||||
// const api = new AutoGPTServerAPI();
|
||||
// const agents = await api.getStoreAgents({ featured: true });
|
||||
// return agents.agents.map((agent) => ({
|
||||
// creator: agent.creator,
|
||||
// slug: agent.slug,
|
||||
// }));
|
||||
// }
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: {
|
||||
params: { creator: string; slug: string };
|
||||
}) {
|
||||
const api = new AutoGPTServerAPI();
|
||||
const agent = await api.getStoreAgent(params.creator, params.slug);
|
||||
const otherAgents = await api.getStoreAgents({ creator: params.creator });
|
||||
const similarAgents = await api.getStoreAgents({
|
||||
search_query: agent.categories[0],
|
||||
});
|
||||
|
||||
const breadcrumbs = [
|
||||
{ name: "Store", link: "/store" },
|
||||
{ name: agent.creator, link: `/store/creator/${agent.creator}` },
|
||||
{ name: agent.agent_name, link: "#" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-screen max-w-[1360px]">
|
||||
<main className="px-4 md:mt-4 lg:mt-8">
|
||||
<BreadCrumbs items={breadcrumbs} />
|
||||
|
||||
<div className="mt-4 flex flex-col items-start gap-4 sm:mt-6 sm:gap-6 md:mt-8 md:flex-row md:gap-8">
|
||||
<div className="w-full md:w-auto md:shrink-0">
|
||||
<AgentInfo
|
||||
name={agent.agent_name}
|
||||
creator={agent.creator}
|
||||
shortDescription={agent.description}
|
||||
longDescription={agent.description}
|
||||
rating={agent.rating}
|
||||
runs={agent.runs}
|
||||
categories={agent.categories}
|
||||
lastUpdated={agent.updated_at}
|
||||
version={agent.versions[agent.versions.length - 1]}
|
||||
/>
|
||||
</div>
|
||||
<AgentImages images={agent.agent_image} />
|
||||
</div>
|
||||
<Separator className="my-6" />
|
||||
<AgentsSection
|
||||
agents={otherAgents.agents}
|
||||
sectionTitle={`Other agents by ${agent.creator}`}
|
||||
/>
|
||||
<Separator className="my-6" />
|
||||
<AgentsSection
|
||||
agents={similarAgents.agents}
|
||||
sectionTitle="Similar agents"
|
||||
/>
|
||||
<BecomeACreator
|
||||
title="Become a Creator"
|
||||
description="Join our ever-growing community of hackers and tinkerers"
|
||||
buttonText="Become a Creator"
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
|
||||
import {
|
||||
CreatorDetails as Creator,
|
||||
StoreAgent,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { AgentsSection } from "@/components/agptui/composite/AgentsSection";
|
||||
import { BreadCrumbs } from "@/components/agptui/BreadCrumbs";
|
||||
import { Metadata } from "next";
|
||||
import { CreatorInfoCard } from "@/components/agptui/CreatorInfoCard";
|
||||
import { CreatorLinks } from "@/components/agptui/CreatorLinks";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { creator: string };
|
||||
}): Promise<Metadata> {
|
||||
const api = new AutoGPTServerAPI();
|
||||
const creator = await api.getStoreCreator(params.creator);
|
||||
|
||||
return {
|
||||
title: `${creator.name} - AutoGPT Store`,
|
||||
description: creator.description,
|
||||
};
|
||||
}
|
||||
|
||||
// export async function generateStaticParams() {
|
||||
// const api = new AutoGPTServerAPI();
|
||||
// const creators = await api.getStoreCreators({ featured: true });
|
||||
// return creators.creators.map((creator) => ({
|
||||
// creator: creator.username,
|
||||
// }));
|
||||
// }
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: {
|
||||
params: { creator: string };
|
||||
}) {
|
||||
const api = new AutoGPTServerAPI();
|
||||
|
||||
try {
|
||||
const creator = await api.getStoreCreator(params.creator);
|
||||
const creatorAgents = await api.getStoreAgents({ creator: params.creator });
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-screen max-w-[1360px]">
|
||||
<main className="px-4 md:mt-4 lg:mt-8">
|
||||
<BreadCrumbs
|
||||
items={[
|
||||
{ name: "Store", link: "/store" },
|
||||
{ name: creator.name, link: "#" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="mt-4 flex flex-col items-start gap-4 sm:mt-6 sm:gap-6 md:mt-8 md:flex-row md:gap-8">
|
||||
<div className="w-full md:w-auto md:shrink-0">
|
||||
<CreatorInfoCard
|
||||
username={creator.name}
|
||||
handle={creator.username}
|
||||
avatarSrc={creator.avatar_url}
|
||||
categories={creator.top_categories}
|
||||
averageRating={creator.agent_rating}
|
||||
totalRuns={creator.agent_runs}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-4 sm:gap-6 md:gap-8">
|
||||
<div className="font-neue text-2xl font-normal leading-normal text-neutral-900 sm:text-3xl md:text-[35px] md:leading-[45px]">
|
||||
{creator.description}
|
||||
</div>
|
||||
<CreatorLinks links={creator.links} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 sm:mt-12 md:mt-16">
|
||||
<hr className="w-full bg-neutral-700" />
|
||||
<AgentsSection
|
||||
agents={creatorAgents.agents}
|
||||
hideAvatars={true}
|
||||
sectionTitle={`Agents by ${creator.name}`}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
} catch (error) {
|
||||
return (
|
||||
<div className="flex h-screen w-full items-center justify-center">
|
||||
<div className="font-neue text-2xl text-neutral-900">
|
||||
Creator not found
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
191
autogpt_platform/frontend/src/app/store/page.tsx
Normal file
191
autogpt_platform/frontend/src/app/store/page.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import * as React from "react";
|
||||
import { HeroSection } from "@/components/agptui/composite/HeroSection";
|
||||
import {
|
||||
FeaturedSection,
|
||||
FeaturedAgent,
|
||||
} from "@/components/agptui/composite/FeaturedSection";
|
||||
import {
|
||||
AgentsSection,
|
||||
Agent,
|
||||
} from "@/components/agptui/composite/AgentsSection";
|
||||
import { BecomeACreator } from "@/components/agptui/BecomeACreator";
|
||||
import {
|
||||
FeaturedCreators,
|
||||
FeaturedCreator,
|
||||
} from "@/components/agptui/composite/FeaturedCreators";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import AutoGPTServerAPIServerSide from "@/lib/autogpt-server-api/clientServer";
|
||||
import { Metadata } from "next";
|
||||
import { createServerClient } from "@/lib/supabase/server";
|
||||
import {
|
||||
StoreAgentsResponse,
|
||||
CreatorsResponse,
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
async function getStoreData() {
|
||||
try {
|
||||
const supabase = createServerClient();
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
|
||||
const api = new AutoGPTServerAPIServerSide(
|
||||
process.env.NEXT_PUBLIC_AGPT_SERVER_URL,
|
||||
process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL,
|
||||
supabase,
|
||||
);
|
||||
|
||||
// Add error handling and default values
|
||||
let featuredAgents: StoreAgentsResponse = {
|
||||
agents: [],
|
||||
pagination: {
|
||||
total_items: 0,
|
||||
total_pages: 0,
|
||||
current_page: 0,
|
||||
page_size: 0,
|
||||
},
|
||||
};
|
||||
let topAgents: StoreAgentsResponse = {
|
||||
agents: [],
|
||||
pagination: {
|
||||
total_items: 0,
|
||||
total_pages: 0,
|
||||
current_page: 0,
|
||||
page_size: 0,
|
||||
},
|
||||
};
|
||||
let featuredCreators: CreatorsResponse = {
|
||||
creators: [],
|
||||
pagination: {
|
||||
total_items: 0,
|
||||
total_pages: 0,
|
||||
current_page: 0,
|
||||
page_size: 0,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
[featuredAgents, topAgents, featuredCreators] = await Promise.all([
|
||||
api.getStoreAgents({ featured: true }),
|
||||
api.getStoreAgents({ sorted_by: "runs" }),
|
||||
api.getStoreCreators({ featured: true, sorted_by: "num_agents" }),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("Error fetching store data:", error);
|
||||
}
|
||||
|
||||
return {
|
||||
featuredAgents,
|
||||
topAgents,
|
||||
featuredCreators,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error in getStoreData:", error);
|
||||
return {
|
||||
featuredAgents: {
|
||||
agents: [],
|
||||
pagination: {
|
||||
total_items: 0,
|
||||
total_pages: 0,
|
||||
current_page: 0,
|
||||
page_size: 0,
|
||||
},
|
||||
},
|
||||
topAgents: {
|
||||
agents: [],
|
||||
pagination: {
|
||||
total_items: 0,
|
||||
total_pages: 0,
|
||||
current_page: 0,
|
||||
page_size: 0,
|
||||
},
|
||||
},
|
||||
featuredCreators: {
|
||||
creators: [],
|
||||
pagination: {
|
||||
total_items: 0,
|
||||
total_pages: 0,
|
||||
current_page: 0,
|
||||
page_size: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// FIX: Correct metadata
|
||||
export const metadata: Metadata = {
|
||||
title: "Agent Store - NextGen AutoGPT",
|
||||
description: "Find and use AI Agents created by our community",
|
||||
applicationName: "NextGen AutoGPT Store",
|
||||
authors: [{ name: "AutoGPT Team" }],
|
||||
keywords: [
|
||||
"AI agents",
|
||||
"automation",
|
||||
"artificial intelligence",
|
||||
"AutoGPT",
|
||||
"marketplace",
|
||||
],
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
},
|
||||
openGraph: {
|
||||
title: "Agent Store - NextGen AutoGPT",
|
||||
description: "Find and use AI Agents created by our community",
|
||||
type: "website",
|
||||
siteName: "NextGen AutoGPT Store",
|
||||
images: [
|
||||
{
|
||||
url: "/images/store-og.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "NextGen AutoGPT Store",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Agent Store - NextGen AutoGPT",
|
||||
description: "Find and use AI Agents created by our community",
|
||||
images: ["/images/store-twitter.png"],
|
||||
},
|
||||
icons: {
|
||||
icon: "/favicon.ico",
|
||||
shortcut: "/favicon-16x16.png",
|
||||
apple: "/apple-touch-icon.png",
|
||||
},
|
||||
};
|
||||
|
||||
export default async function Page({}: {}) {
|
||||
// Get data server-side
|
||||
const { featuredAgents, topAgents, featuredCreators } = await getStoreData();
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-screen max-w-[1360px]">
|
||||
<main className="px-4">
|
||||
<HeroSection />
|
||||
<FeaturedSection
|
||||
featuredAgents={featuredAgents.agents as FeaturedAgent[]}
|
||||
/>
|
||||
<Separator />
|
||||
<AgentsSection
|
||||
sectionTitle="Top Agents"
|
||||
agents={topAgents.agents as Agent[]}
|
||||
/>
|
||||
<Separator />
|
||||
<FeaturedCreators
|
||||
featuredCreators={featuredCreators.creators as FeaturedCreator[]}
|
||||
/>
|
||||
<Separator />
|
||||
<BecomeACreator
|
||||
title="Become a Creator"
|
||||
description="Join our ever-growing community of hackers and tinkerers"
|
||||
buttonText="Become a Creator"
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
182
autogpt_platform/frontend/src/app/store/search/page.tsx
Normal file
182
autogpt_platform/frontend/src/app/store/search/page.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { AutoGPTServerAPI } from "@/lib/autogpt-server-api/client";
|
||||
import { AgentsSection } from "@/components/agptui/composite/AgentsSection";
|
||||
import { SearchBar } from "@/components/agptui/SearchBar";
|
||||
import { FeaturedCreators } from "@/components/agptui/composite/FeaturedCreators";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { SearchFilterChips } from "@/components/agptui/SearchFilterChips";
|
||||
import { SortDropdown } from "@/components/agptui/SortDropdown";
|
||||
|
||||
export default function Page({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { searchTerm?: string; sort?: string };
|
||||
}) {
|
||||
return (
|
||||
<SearchResults
|
||||
searchTerm={searchParams.searchTerm || ""}
|
||||
sort={searchParams.sort || "trending"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SearchResults({
|
||||
searchTerm,
|
||||
sort,
|
||||
}: {
|
||||
searchTerm: string;
|
||||
sort: string;
|
||||
}) {
|
||||
const [showAgents, setShowAgents] = useState(true);
|
||||
const [showCreators, setShowCreators] = useState(true);
|
||||
const [agents, setAgents] = useState<any[]>([]);
|
||||
const [creators, setCreators] = useState<any[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
const api = new AutoGPTServerAPI();
|
||||
|
||||
try {
|
||||
const [agentsRes, creatorsRes] = await Promise.all([
|
||||
api.getStoreAgents({
|
||||
search_query: searchTerm,
|
||||
sorted_by: sort,
|
||||
}),
|
||||
api.getStoreCreators({
|
||||
search_query: searchTerm,
|
||||
}),
|
||||
]);
|
||||
|
||||
setAgents(agentsRes.agents || []);
|
||||
setCreators(creatorsRes.creators || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [searchTerm, sort]);
|
||||
|
||||
const agentsCount = agents.length;
|
||||
const creatorsCount = creators.length;
|
||||
const totalCount = agentsCount + creatorsCount;
|
||||
|
||||
const handleFilterChange = (value: string) => {
|
||||
if (value === "agents") {
|
||||
setShowAgents(true);
|
||||
setShowCreators(false);
|
||||
} else if (value === "creators") {
|
||||
setShowAgents(false);
|
||||
setShowCreators(true);
|
||||
} else {
|
||||
setShowAgents(true);
|
||||
setShowCreators(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSortChange = (sortValue: string) => {
|
||||
let sortBy = "recent";
|
||||
if (sortValue === "runs") {
|
||||
sortBy = "runs";
|
||||
} else if (sortValue === "rating") {
|
||||
sortBy = "rating";
|
||||
}
|
||||
|
||||
const sortedAgents = [...agents].sort((a, b) => {
|
||||
if (sortBy === "runs") {
|
||||
return b.runs - a.runs;
|
||||
} else if (sortBy === "rating") {
|
||||
return b.rating - a.rating;
|
||||
} else {
|
||||
return (
|
||||
new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const sortedCreators = [...creators].sort((a, b) => {
|
||||
if (sortBy === "runs") {
|
||||
return b.agent_runs - a.agent_runs;
|
||||
} else if (sortBy === "rating") {
|
||||
return b.agent_rating - a.agent_rating;
|
||||
} else {
|
||||
// Creators don't have updated_at, sort by number of agents as fallback
|
||||
return b.num_agents - a.num_agents;
|
||||
}
|
||||
});
|
||||
|
||||
setAgents(sortedAgents);
|
||||
setCreators(sortedCreators);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mx-auto min-h-screen max-w-[1440px] px-10 lg:min-w-[1440px]">
|
||||
<div className="mt-8 flex items-center">
|
||||
<div className="flex-1">
|
||||
<h2 className="font-geist text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
Results for:
|
||||
</h2>
|
||||
<h1 className="font-poppins text-2xl font-semibold leading-loose text-neutral-800 dark:text-neutral-100">
|
||||
{searchTerm}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<SearchBar width="w-[439px]" height="h-[60px]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="mt-20 flex flex-col items-center justify-center">
|
||||
<p className="text-neutral-500 dark:text-neutral-400">Loading...</p>
|
||||
</div>
|
||||
) : totalCount > 0 ? (
|
||||
<>
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<SearchFilterChips
|
||||
totalCount={totalCount}
|
||||
agentsCount={agentsCount}
|
||||
creatorsCount={creatorsCount}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
<SortDropdown onSort={handleSortChange} />
|
||||
</div>
|
||||
{/* Content section */}
|
||||
<div className="min-h-[500px] max-w-[1440px]">
|
||||
{showAgents && agentsCount > 0 && (
|
||||
<div className="mt-8">
|
||||
<AgentsSection agents={agents} sectionTitle="Agents" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAgents && agentsCount > 0 && creatorsCount > 0 && (
|
||||
<Separator />
|
||||
)}
|
||||
{showCreators && creatorsCount > 0 && (
|
||||
<FeaturedCreators
|
||||
featuredCreators={creators}
|
||||
title="Creators"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-20 flex flex-col items-center justify-center">
|
||||
<h3 className="mb-2 text-xl font-medium text-neutral-600 dark:text-neutral-300">
|
||||
No results found
|
||||
</h3>
|
||||
<p className="text-neutral-500 dark:text-neutral-400">
|
||||
Try adjusting your search terms or filters
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
NodeExecutionResult,
|
||||
BlockUIType,
|
||||
BlockCost,
|
||||
useBackendAPI,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import {
|
||||
beautifyString,
|
||||
cn,
|
||||
@@ -258,7 +258,7 @@ export function CustomNode({
|
||||
) : (
|
||||
propKey != "credentials" && (
|
||||
<div className="flex gap-1">
|
||||
<span className="text-m green mb-0 text-gray-900">
|
||||
<span className="text-m green mb-0 text-gray-900 dark:text-gray-100">
|
||||
{propSchema.title || beautifyString(propKey)}
|
||||
</span>
|
||||
<SchemaTooltip description={propSchema.description} />
|
||||
@@ -460,49 +460,54 @@ export function CustomNode({
|
||||
"custom-node",
|
||||
"dark-theme",
|
||||
"rounded-xl",
|
||||
"bg-white/[.9]",
|
||||
"border border-gray-300",
|
||||
"bg-white/[.9] dark:bg-gray-800/[.9]",
|
||||
"border border-gray-300 dark:border-gray-600",
|
||||
data.uiType === BlockUIType.NOTE ? "w-[300px]" : "w-[500px]",
|
||||
data.uiType === BlockUIType.NOTE ? "bg-yellow-100" : "bg-white",
|
||||
data.uiType === BlockUIType.NOTE
|
||||
? "bg-yellow-100 dark:bg-yellow-900"
|
||||
: "bg-white dark:bg-gray-800",
|
||||
selected ? "shadow-2xl" : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
const errorClass =
|
||||
hasConfigErrors || hasOutputError ? "border-red-200 border-2" : "";
|
||||
hasConfigErrors || hasOutputError
|
||||
? "border-red-200 dark:border-red-800 border-2"
|
||||
: "";
|
||||
|
||||
const statusClass = (() => {
|
||||
if (hasConfigErrors || hasOutputError) return "border-red-200 border-4";
|
||||
if (hasConfigErrors || hasOutputError)
|
||||
return "border-red-200 dark:border-red-800 border-4";
|
||||
switch (data.status?.toLowerCase()) {
|
||||
case "completed":
|
||||
return "border-green-200 border-4";
|
||||
return "border-green-200 dark:border-green-800 border-4";
|
||||
case "running":
|
||||
return "border-yellow-200 border-4";
|
||||
return "border-yellow-200 dark:border-yellow-800 border-4";
|
||||
case "failed":
|
||||
return "border-red-200 border-4";
|
||||
return "border-red-200 dark:border-red-800 border-4";
|
||||
case "incomplete":
|
||||
return "border-purple-200 border-4";
|
||||
return "border-purple-200 dark:border-purple-800 border-4";
|
||||
case "queued":
|
||||
return "border-cyan-200 border-4";
|
||||
return "border-cyan-200 dark:border-cyan-800 border-4";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
})();
|
||||
|
||||
const statusBackgroundClass = (() => {
|
||||
if (hasConfigErrors || hasOutputError) return "bg-red-200";
|
||||
if (hasConfigErrors || hasOutputError) return "bg-red-200 dark:bg-red-800";
|
||||
switch (data.status?.toLowerCase()) {
|
||||
case "completed":
|
||||
return "bg-green-200";
|
||||
return "bg-green-200 dark:bg-green-800";
|
||||
case "running":
|
||||
return "bg-yellow-200";
|
||||
return "bg-yellow-200 dark:bg-yellow-800";
|
||||
case "failed":
|
||||
return "bg-red-200";
|
||||
return "bg-red-200 dark:bg-red-800";
|
||||
case "incomplete":
|
||||
return "bg-purple-200";
|
||||
return "bg-purple-200 dark:bg-purple-800";
|
||||
case "queued":
|
||||
return "bg-cyan-200";
|
||||
return "bg-cyan-200 dark:bg-cyan-800";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -591,36 +596,36 @@ export function CustomNode({
|
||||
);
|
||||
|
||||
const LineSeparator = () => (
|
||||
<div className="bg-white pt-6">
|
||||
<Separator.Root className="h-[1px] w-full bg-gray-300"></Separator.Root>
|
||||
<div className="bg-white pt-6 dark:bg-gray-800">
|
||||
<Separator.Root className="h-[1px] w-full bg-gray-300 dark:bg-gray-600"></Separator.Root>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ContextMenuContent = () => (
|
||||
<ContextMenu.Content className="z-10 rounded-xl border bg-white p-1 shadow-md">
|
||||
<ContextMenu.Content className="z-10 rounded-xl border bg-white p-1 shadow-md dark:bg-gray-800">
|
||||
<ContextMenu.Item
|
||||
onSelect={copyNode}
|
||||
className="flex cursor-pointer items-center rounded-md px-3 py-2 hover:bg-gray-100"
|
||||
className="flex cursor-pointer items-center rounded-md px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<CopyIcon className="mr-2 h-5 w-5" />
|
||||
<span>Copy</span>
|
||||
<CopyIcon className="mr-2 h-5 w-5 dark:text-gray-100" />
|
||||
<span className="dark:text-gray-100">Copy</span>
|
||||
</ContextMenu.Item>
|
||||
{nodeFlowId && (
|
||||
<ContextMenu.Item
|
||||
onSelect={() => window.open(`/build?flowID=${nodeFlowId}`)}
|
||||
className="flex cursor-pointer items-center rounded-md px-3 py-2 hover:bg-gray-100"
|
||||
className="flex cursor-pointer items-center rounded-md px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<ExitIcon className="mr-2 h-5 w-5" />
|
||||
<span>Open agent</span>
|
||||
<ExitIcon className="mr-2 h-5 w-5 dark:text-gray-100" />
|
||||
<span className="dark:text-gray-100">Open agent</span>
|
||||
</ContextMenu.Item>
|
||||
)}
|
||||
<ContextMenu.Separator className="my-1 h-px bg-gray-300" />
|
||||
<ContextMenu.Separator className="my-1 h-px bg-gray-300 dark:bg-gray-600" />
|
||||
<ContextMenu.Item
|
||||
onSelect={deleteNode}
|
||||
className="flex cursor-pointer items-center rounded-md px-3 py-2 text-red-500 hover:bg-gray-100"
|
||||
className="flex cursor-pointer items-center rounded-md px-3 py-2 text-red-500 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<TrashIcon className="mr-2 h-5 w-5 text-red-500" />
|
||||
<span>Delete</span>
|
||||
<TrashIcon className="mr-2 h-5 w-5 text-red-500 dark:text-red-400" />
|
||||
<span className="dark:text-red-400">Delete</span>
|
||||
</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
);
|
||||
|
||||
@@ -70,9 +70,8 @@ export const FlowContext = createContext<FlowContextType | null>(null);
|
||||
|
||||
const FlowEditor: React.FC<{
|
||||
flowID?: string;
|
||||
template?: boolean;
|
||||
className?: string;
|
||||
}> = ({ flowID, template, className }) => {
|
||||
}> = ({ flowID, className }) => {
|
||||
const {
|
||||
addNodes,
|
||||
addEdges,
|
||||
@@ -106,7 +105,7 @@ const FlowEditor: React.FC<{
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = useAgentGraph(flowID, template, visualizeBeads !== "no");
|
||||
} = useAgentGraph(flowID, visualizeBeads !== "no");
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
@@ -661,9 +660,10 @@ const FlowEditor: React.FC<{
|
||||
deleteKeyCode={["Backspace", "Delete"]}
|
||||
minZoom={0.2}
|
||||
maxZoom={2}
|
||||
className="dark:bg-slate-900"
|
||||
>
|
||||
<Controls />
|
||||
<Background />
|
||||
<Background className="dark:bg-slate-800" />
|
||||
<ControlPanel
|
||||
className="absolute z-10"
|
||||
controls={editorControls}
|
||||
|
||||
@@ -35,7 +35,7 @@ const NodeHandle: FC<HandleProps> = ({
|
||||
|
||||
const label = (
|
||||
<div className="flex flex-grow flex-row">
|
||||
<span className="text-m green flex items-end pr-2 text-gray-900">
|
||||
<span className="text-m green flex items-end pr-2 text-gray-900 dark:text-gray-100">
|
||||
{title || schema.title || beautifyString(keyName.toLowerCase())}
|
||||
{isRequired ? "*" : ""}
|
||||
</span>
|
||||
@@ -48,10 +48,10 @@ const NodeHandle: FC<HandleProps> = ({
|
||||
const Dot = () => {
|
||||
const color = isConnected
|
||||
? getTypeBgColor(schema.type || "any")
|
||||
: "border-gray-300";
|
||||
: "border-gray-300 dark:border-gray-600";
|
||||
return (
|
||||
<div
|
||||
className={`${color} m-1 h-4 w-4 rounded-full border-2 bg-white transition-colors duration-100 group-hover:bg-gray-300`}
|
||||
className={`${color} m-1 h-4 w-4 rounded-full border-2 bg-white transition-colors duration-100 group-hover:bg-gray-300 dark:bg-slate-800 dark:group-hover:bg-gray-700`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "./ui/button";
|
||||
import { useSupabase } from "./SupabaseProvider";
|
||||
import { useSupabase } from "./providers/SupabaseProvider";
|
||||
import { useRouter } from "next/navigation";
|
||||
import useUser from "@/hooks/useUser";
|
||||
|
||||
|
||||
@@ -14,15 +14,18 @@ const SchemaTooltip: React.FC<{ description?: string }> = ({ description }) => {
|
||||
<TooltipProvider delayDuration={400}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="rounded-full p-1 hover:bg-gray-300" size={24} />
|
||||
<Info
|
||||
className="rounded-full p-1 hover:bg-gray-300 dark:hover:bg-gray-700"
|
||||
size={24}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="tooltip-content max-w-xs">
|
||||
<TooltipContent className="tooltip-content max-w-xs bg-white text-gray-900 dark:bg-gray-800 dark:text-gray-100">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
a: ({ node, ...props }) => (
|
||||
<a
|
||||
target="_blank"
|
||||
className="text-blue-400 underline"
|
||||
className="text-blue-400 underline dark:text-blue-300"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button } from "./ui/button";
|
||||
import { QuestionMarkCircledIcon } from "@radix-ui/react-icons";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
|
||||
const TallyPopupSimple = () => {
|
||||
const [isFormVisible, setIsFormVisible] = useState(false);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const [show_tutorial, setShowTutorial] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setShowTutorial(pathname.includes("build"));
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
// Load Tally script
|
||||
@@ -49,21 +57,23 @@ const TallyPopupSimple = () => {
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-1 right-6 z-50 hidden select-none items-center gap-4 p-3 transition-all duration-300 ease-in-out md:flex">
|
||||
{show_tutorial && (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={resetTutorial}
|
||||
className="font-inter mb-0 h-14 w-28 rounded-2xl bg-[rgba(65,65,64,1)] text-left text-lg font-medium leading-6"
|
||||
>
|
||||
Tutorial
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={resetTutorial}
|
||||
className="font-inter mb-0 h-14 w-28 rounded-2xl bg-[rgba(65,65,64,1)] text-left text-lg font-medium leading-6"
|
||||
>
|
||||
Tutorial
|
||||
</Button>
|
||||
<Button
|
||||
className="h-14 w-14 rounded-2xl bg-[rgba(65,65,64,1)]"
|
||||
className="h-14 w-14 rounded-full bg-[rgba(65,65,64,1)]"
|
||||
variant="default"
|
||||
data-tally-open="3yx2L0"
|
||||
data-tally-emoji-text="👋"
|
||||
data-tally-emoji-animation="wave"
|
||||
>
|
||||
<QuestionMarkCircledIcon className="h-6 w-6" />
|
||||
<QuestionMarkCircledIcon className="h-14 w-14" />
|
||||
<span className="sr-only">Reach Out</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,149 +1,149 @@
|
||||
"use client";
|
||||
// "use client";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogClose,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
MultiSelector,
|
||||
MultiSelectorContent,
|
||||
MultiSelectorInput,
|
||||
MultiSelectorItem,
|
||||
MultiSelectorList,
|
||||
MultiSelectorTrigger,
|
||||
} from "@/components/ui/multiselect";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useState } from "react";
|
||||
import { addFeaturedAgent } from "./actions";
|
||||
import { Agent } from "@/lib/marketplace-api/types";
|
||||
// import {
|
||||
// Dialog,
|
||||
// DialogContent,
|
||||
// DialogClose,
|
||||
// DialogFooter,
|
||||
// DialogHeader,
|
||||
// DialogTitle,
|
||||
// DialogTrigger,
|
||||
// } from "@/components/ui/dialog";
|
||||
// import { Button } from "@/components/ui/button";
|
||||
// import {
|
||||
// MultiSelector,
|
||||
// MultiSelectorContent,
|
||||
// MultiSelectorInput,
|
||||
// MultiSelectorItem,
|
||||
// MultiSelectorList,
|
||||
// MultiSelectorTrigger,
|
||||
// } from "@/components/ui/multiselect";
|
||||
// import { Controller, useForm } from "react-hook-form";
|
||||
// import {
|
||||
// Select,
|
||||
// SelectContent,
|
||||
// SelectItem,
|
||||
// SelectTrigger,
|
||||
// SelectValue,
|
||||
// } from "@/components/ui/select";
|
||||
// import { useState } from "react";
|
||||
// import { addFeaturedAgent } from "./actions";
|
||||
// import { Agent } from "@/lib/marketplace-api/types";
|
||||
|
||||
type FormData = {
|
||||
agent: string;
|
||||
categories: string[];
|
||||
};
|
||||
// type FormData = {
|
||||
// agent: string;
|
||||
// categories: string[];
|
||||
// };
|
||||
|
||||
export const AdminAddFeaturedAgentDialog = ({
|
||||
categories,
|
||||
agents,
|
||||
}: {
|
||||
categories: string[];
|
||||
agents: Agent[];
|
||||
}) => {
|
||||
const [selectedAgent, setSelectedAgent] = useState<string>("");
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||
// export const AdminAddFeaturedAgentDialog = ({
|
||||
// categories,
|
||||
// agents,
|
||||
// }: {
|
||||
// categories: string[];
|
||||
// agents: Agent[];
|
||||
// }) => {
|
||||
// const [selectedAgent, setSelectedAgent] = useState<string>("");
|
||||
// const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<FormData>({
|
||||
defaultValues: {
|
||||
agent: "",
|
||||
categories: [],
|
||||
},
|
||||
});
|
||||
// const {
|
||||
// control,
|
||||
// handleSubmit,
|
||||
// watch,
|
||||
// setValue,
|
||||
// formState: { errors },
|
||||
// } = useForm<FormData>({
|
||||
// defaultValues: {
|
||||
// agent: "",
|
||||
// categories: [],
|
||||
// },
|
||||
// });
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
Add Featured Agent
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Featured Agent</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Controller
|
||||
name="agent"
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
render={({ field }) => (
|
||||
<div>
|
||||
<label htmlFor={field.name}>Agent</label>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
setSelectedAgent(value);
|
||||
}}
|
||||
value={field.value || ""}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an agent" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{/* Populate with agents */}
|
||||
{agents.map((agent) => (
|
||||
<SelectItem key={agent.id} value={agent.id}>
|
||||
{agent.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="categories"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<MultiSelector
|
||||
values={field.value || []}
|
||||
onValuesChange={(values) => {
|
||||
field.onChange(values);
|
||||
setSelectedCategories(values);
|
||||
}}
|
||||
>
|
||||
<MultiSelectorTrigger>
|
||||
<MultiSelectorInput placeholder="Select categories" />
|
||||
</MultiSelectorTrigger>
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
{categories.map((category) => (
|
||||
<MultiSelectorItem key={category} value={category}>
|
||||
{category}
|
||||
</MultiSelectorItem>
|
||||
))}
|
||||
</MultiSelectorList>
|
||||
</MultiSelectorContent>
|
||||
</MultiSelector>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={async () => {
|
||||
// Handle adding the featured agent
|
||||
await addFeaturedAgent(selectedAgent, selectedCategories);
|
||||
// close the dialog
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
// return (
|
||||
// <Dialog>
|
||||
// <DialogTrigger asChild>
|
||||
// <Button variant="outline" size="sm">
|
||||
// Add Featured Agent
|
||||
// </Button>
|
||||
// </DialogTrigger>
|
||||
// <DialogContent>
|
||||
// <DialogHeader>
|
||||
// <DialogTitle>Add Featured Agent</DialogTitle>
|
||||
// </DialogHeader>
|
||||
// <div className="flex flex-col gap-4">
|
||||
// <Controller
|
||||
// name="agent"
|
||||
// control={control}
|
||||
// rules={{ required: true }}
|
||||
// render={({ field }) => (
|
||||
// <div>
|
||||
// <label htmlFor={field.name}>Agent</label>
|
||||
// <Select
|
||||
// onValueChange={(value) => {
|
||||
// field.onChange(value);
|
||||
// setSelectedAgent(value);
|
||||
// }}
|
||||
// value={field.value || ""}
|
||||
// >
|
||||
// <SelectTrigger>
|
||||
// <SelectValue placeholder="Select an agent" />
|
||||
// </SelectTrigger>
|
||||
// <SelectContent>
|
||||
// {/* Populate with agents */}
|
||||
// {agents.map((agent) => (
|
||||
// <SelectItem key={agent.id} value={agent.id}>
|
||||
// {agent.name}
|
||||
// </SelectItem>
|
||||
// ))}
|
||||
// </SelectContent>
|
||||
// </Select>
|
||||
// </div>
|
||||
// )}
|
||||
// />
|
||||
// <Controller
|
||||
// name="categories"
|
||||
// control={control}
|
||||
// render={({ field }) => (
|
||||
// <MultiSelector
|
||||
// values={field.value || []}
|
||||
// onValuesChange={(values) => {
|
||||
// field.onChange(values);
|
||||
// setSelectedCategories(values);
|
||||
// }}
|
||||
// >
|
||||
// <MultiSelectorTrigger>
|
||||
// <MultiSelectorInput placeholder="Select categories" />
|
||||
// </MultiSelectorTrigger>
|
||||
// <MultiSelectorContent>
|
||||
// <MultiSelectorList>
|
||||
// {categories.map((category) => (
|
||||
// <MultiSelectorItem key={category} value={category}>
|
||||
// {category}
|
||||
// </MultiSelectorItem>
|
||||
// ))}
|
||||
// </MultiSelectorList>
|
||||
// </MultiSelectorContent>
|
||||
// </MultiSelector>
|
||||
// )}
|
||||
// />
|
||||
// </div>
|
||||
// <DialogFooter>
|
||||
// <DialogClose asChild>
|
||||
// <Button variant="outline">Cancel</Button>
|
||||
// </DialogClose>
|
||||
// <DialogClose asChild>
|
||||
// <Button
|
||||
// type="submit"
|
||||
// onClick={async () => {
|
||||
// // Handle adding the featured agent
|
||||
// await addFeaturedAgent(selectedAgent, selectedCategories);
|
||||
// // close the dialog
|
||||
// }}
|
||||
// >
|
||||
// Add
|
||||
// </Button>
|
||||
// </DialogClose>
|
||||
// </DialogFooter>
|
||||
// </DialogContent>
|
||||
// </Dialog>
|
||||
// );
|
||||
// };
|
||||
|
||||
@@ -1,74 +1,74 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
getFeaturedAgents,
|
||||
removeFeaturedAgent,
|
||||
getCategories,
|
||||
getNotFeaturedAgents,
|
||||
} from "./actions";
|
||||
// import { Button } from "@/components/ui/button";
|
||||
// import {
|
||||
// getFeaturedAgents,
|
||||
// removeFeaturedAgent,
|
||||
// getCategories,
|
||||
// getNotFeaturedAgents,
|
||||
// } from "./actions";
|
||||
|
||||
import FeaturedAgentsTable from "./FeaturedAgentsTable";
|
||||
import { AdminAddFeaturedAgentDialog } from "./AdminAddFeaturedAgentDialog";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
// import FeaturedAgentsTable from "./FeaturedAgentsTable";
|
||||
// import { AdminAddFeaturedAgentDialog } from "./AdminAddFeaturedAgentDialog";
|
||||
// import { revalidatePath } from "next/cache";
|
||||
// import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
export default async function AdminFeaturedAgentsControl({
|
||||
className,
|
||||
}: {
|
||||
className?: string;
|
||||
}) {
|
||||
// add featured agent button
|
||||
// modal to select agent?
|
||||
// modal to select categories?
|
||||
// table of featured agents
|
||||
// in table
|
||||
// remove featured agent button
|
||||
// edit featured agent categories button
|
||||
// table footer
|
||||
// Next page button
|
||||
// Previous page button
|
||||
// Page number input
|
||||
// Page size input
|
||||
// Total pages input
|
||||
// Go to page button
|
||||
// export default async function AdminFeaturedAgentsControl({
|
||||
// className,
|
||||
// }: {
|
||||
// className?: string;
|
||||
// }) {
|
||||
// // add featured agent button
|
||||
// // modal to select agent?
|
||||
// // modal to select categories?
|
||||
// // table of featured agents
|
||||
// // in table
|
||||
// // remove featured agent button
|
||||
// // edit featured agent categories button
|
||||
// // table footer
|
||||
// // Next page button
|
||||
// // Previous page button
|
||||
// // Page number input
|
||||
// // Page size input
|
||||
// // Total pages input
|
||||
// // Go to page button
|
||||
|
||||
const page = 1;
|
||||
const pageSize = 10;
|
||||
// const page = 1;
|
||||
// const pageSize = 10;
|
||||
|
||||
const agents = await getFeaturedAgents(page, pageSize);
|
||||
// const agents = await getFeaturedAgents(page, pageSize);
|
||||
|
||||
const categories = await getCategories();
|
||||
// const categories = await getCategories();
|
||||
|
||||
const notFeaturedAgents = await getNotFeaturedAgents();
|
||||
// const notFeaturedAgents = await getNotFeaturedAgents();
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-4 ${className}`}>
|
||||
<div className="mb-4 flex justify-between">
|
||||
<h3 className="text-lg font-semibold">Featured Agent Controls</h3>
|
||||
<AdminAddFeaturedAgentDialog
|
||||
categories={categories.unique_categories}
|
||||
agents={notFeaturedAgents.items}
|
||||
/>
|
||||
</div>
|
||||
<FeaturedAgentsTable
|
||||
agents={agents.items}
|
||||
globalActions={[
|
||||
{
|
||||
component: <Button>Remove</Button>,
|
||||
action: async (rows) => {
|
||||
"use server";
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"removeFeaturedAgent",
|
||||
{},
|
||||
async () => {
|
||||
const all = rows.map((row) => removeFeaturedAgent(row.id));
|
||||
await Promise.all(all);
|
||||
revalidatePath("/marketplace");
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// return (
|
||||
// <div className={`flex flex-col gap-4 ${className}`}>
|
||||
// <div className="mb-4 flex justify-between">
|
||||
// <h3 className="text-lg font-semibold">Featured Agent Controls</h3>
|
||||
// <AdminAddFeaturedAgentDialog
|
||||
// categories={categories.unique_categories}
|
||||
// agents={notFeaturedAgents.items}
|
||||
// />
|
||||
// </div>
|
||||
// <FeaturedAgentsTable
|
||||
// agents={agents.items}
|
||||
// globalActions={[
|
||||
// {
|
||||
// component: <Button>Remove</Button>,
|
||||
// action: async (rows) => {
|
||||
// "use server";
|
||||
// return await Sentry.withServerActionInstrumentation(
|
||||
// "removeFeaturedAgent",
|
||||
// {},
|
||||
// async () => {
|
||||
// const all = rows.map((row) => removeFeaturedAgent(row.id));
|
||||
// await Promise.all(all);
|
||||
// revalidatePath("/marketplace");
|
||||
// },
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
// ]}
|
||||
// />
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
import { Agent } from "@/lib/marketplace-api";
|
||||
import AdminMarketplaceCard from "./AdminMarketplaceCard";
|
||||
import { ClipboardX } from "lucide-react";
|
||||
// import { Agent } from "@/lib/marketplace-api";
|
||||
// import AdminMarketplaceCard from "./AdminMarketplaceCard";
|
||||
// import { ClipboardX } from "lucide-react";
|
||||
|
||||
export default function AdminMarketplaceAgentList({
|
||||
agents,
|
||||
className,
|
||||
}: {
|
||||
agents: Agent[];
|
||||
className?: string;
|
||||
}) {
|
||||
if (agents.length === 0) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<h3 className="text-lg font-semibold">Agents to review</h3>
|
||||
<div className="flex flex-col items-center justify-center py-12 text-gray-500">
|
||||
<ClipboardX size={48} />
|
||||
<p className="mt-4 text-lg font-semibold">No agents to review</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// export default function AdminMarketplaceAgentList({
|
||||
// agents,
|
||||
// className,
|
||||
// }: {
|
||||
// agents: Agent[];
|
||||
// className?: string;
|
||||
// }) {
|
||||
// if (agents.length === 0) {
|
||||
// return (
|
||||
// <div className={className}>
|
||||
// <h3 className="text-lg font-semibold">Agents to review</h3>
|
||||
// <div className="flex flex-col items-center justify-center py-12 text-gray-500">
|
||||
// <ClipboardX size={48} />
|
||||
// <p className="mt-4 text-lg font-semibold">No agents to review</p>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-4 ${className}`}>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Agents to review</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
{agents.map((agent) => (
|
||||
<AdminMarketplaceCard agent={agent} key={agent.id} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// return (
|
||||
// <div className={`flex flex-col gap-4 ${className}`}>
|
||||
// <div>
|
||||
// <h3 className="text-lg font-semibold">Agents to review</h3>
|
||||
// </div>
|
||||
// <div className="flex flex-col gap-4">
|
||||
// {agents.map((agent) => (
|
||||
// <AdminMarketplaceCard agent={agent} key={agent.id} />
|
||||
// ))}
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
@@ -1,113 +1,113 @@
|
||||
"use client";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { approveAgent, rejectAgent } from "./actions";
|
||||
import { Agent } from "@/lib/marketplace-api";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
// "use client";
|
||||
// import { Card } from "@/components/ui/card";
|
||||
// import { Button } from "@/components/ui/button";
|
||||
// import { Badge } from "@/components/ui/badge";
|
||||
// import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
// import { approveAgent, rejectAgent } from "./actions";
|
||||
// import { Agent } from "@/lib/marketplace-api";
|
||||
// import Link from "next/link";
|
||||
// import { useState } from "react";
|
||||
// import { Input } from "@/components/ui/input";
|
||||
|
||||
function AdminMarketplaceCard({ agent }: { agent: Agent }) {
|
||||
const [isApproved, setIsApproved] = useState(false);
|
||||
const [isRejected, setIsRejected] = useState(false);
|
||||
const [comment, setComment] = useState("");
|
||||
// function AdminMarketplaceCard({ agent }: { agent: Agent }) {
|
||||
// const [isApproved, setIsApproved] = useState(false);
|
||||
// const [isRejected, setIsRejected] = useState(false);
|
||||
// const [comment, setComment] = useState("");
|
||||
|
||||
const approveAgentWithId = approveAgent.bind(
|
||||
null,
|
||||
agent.id,
|
||||
agent.version,
|
||||
comment,
|
||||
);
|
||||
const rejectAgentWithId = rejectAgent.bind(
|
||||
null,
|
||||
agent.id,
|
||||
agent.version,
|
||||
comment,
|
||||
);
|
||||
// const approveAgentWithId = approveAgent.bind(
|
||||
// null,
|
||||
// agent.id,
|
||||
// agent.version,
|
||||
// comment,
|
||||
// );
|
||||
// const rejectAgentWithId = rejectAgent.bind(
|
||||
// null,
|
||||
// agent.id,
|
||||
// agent.version,
|
||||
// comment,
|
||||
// );
|
||||
|
||||
const handleApprove = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
await approveAgentWithId();
|
||||
setIsApproved(true);
|
||||
};
|
||||
// const handleApprove = async (e: React.FormEvent) => {
|
||||
// e.preventDefault();
|
||||
// await approveAgentWithId();
|
||||
// setIsApproved(true);
|
||||
// };
|
||||
|
||||
const handleReject = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
await rejectAgentWithId();
|
||||
setIsRejected(true);
|
||||
};
|
||||
// const handleReject = async (e: React.FormEvent) => {
|
||||
// e.preventDefault();
|
||||
// await rejectAgentWithId();
|
||||
// setIsRejected(true);
|
||||
// };
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isApproved && !isRejected && (
|
||||
<Card key={agent.id} className="m-3 flex h-[300px] flex-col p-4">
|
||||
<div className="mb-2 flex items-start justify-between">
|
||||
<Link
|
||||
href={`/marketplace/${agent.id}`}
|
||||
className="text-lg font-semibold hover:underline"
|
||||
>
|
||||
{agent.name}
|
||||
</Link>
|
||||
<Badge variant="outline">v{agent.version}</Badge>
|
||||
</div>
|
||||
<p className="mb-2 text-sm text-gray-500">by {agent.author}</p>
|
||||
<ScrollArea className="flex-grow">
|
||||
<p className="mb-2 text-sm text-gray-600">{agent.description}</p>
|
||||
<div className="mb-2 flex flex-wrap gap-1">
|
||||
{agent.categories.map((category) => (
|
||||
<Badge key={category} variant="secondary">
|
||||
{category}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{agent.keywords.map((keyword) => (
|
||||
<Badge key={keyword} variant="outline">
|
||||
{keyword}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<div className="mb-2 flex justify-between text-xs text-gray-500">
|
||||
<span>
|
||||
Created: {new Date(agent.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
<span>
|
||||
Updated: {new Date(agent.updatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mb-4 flex justify-between text-sm">
|
||||
<span>👁 {agent.views}</span>
|
||||
<span>⬇️ {agent.downloads}</span>
|
||||
</div>
|
||||
<div className="mt-auto space-y-2">
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Add a comment (optional)"
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
/>
|
||||
{!isRejected && (
|
||||
<form onSubmit={handleReject}>
|
||||
<Button variant="outline" type="submit">
|
||||
Reject
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
{!isApproved && (
|
||||
<form onSubmit={handleApprove}>
|
||||
<Button type="submit">Approve</Button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
// return (
|
||||
// <>
|
||||
// {!isApproved && !isRejected && (
|
||||
// <Card key={agent.id} className="m-3 flex h-[300px] flex-col p-4">
|
||||
// <div className="mb-2 flex items-start justify-between">
|
||||
// <Link
|
||||
// href={`/marketplace/${agent.id}`}
|
||||
// className="text-lg font-semibold hover:underline"
|
||||
// >
|
||||
// {agent.name}
|
||||
// </Link>
|
||||
// <Badge variant="outline">v{agent.version}</Badge>
|
||||
// </div>
|
||||
// <p className="mb-2 text-sm text-gray-500">by {agent.author}</p>
|
||||
// <ScrollArea className="flex-grow">
|
||||
// <p className="mb-2 text-sm text-gray-600">{agent.description}</p>
|
||||
// <div className="mb-2 flex flex-wrap gap-1">
|
||||
// {agent.categories.map((category) => (
|
||||
// <Badge key={category} variant="secondary">
|
||||
// {category}
|
||||
// </Badge>
|
||||
// ))}
|
||||
// </div>
|
||||
// <div className="flex flex-wrap gap-1">
|
||||
// {agent.keywords.map((keyword) => (
|
||||
// <Badge key={keyword} variant="outline">
|
||||
// {keyword}
|
||||
// </Badge>
|
||||
// ))}
|
||||
// </div>
|
||||
// </ScrollArea>
|
||||
// <div className="mb-2 flex justify-between text-xs text-gray-500">
|
||||
// <span>
|
||||
// Created: {new Date(agent.createdAt).toLocaleDateString()}
|
||||
// </span>
|
||||
// <span>
|
||||
// Updated: {new Date(agent.updatedAt).toLocaleDateString()}
|
||||
// </span>
|
||||
// </div>
|
||||
// <div className="mb-4 flex justify-between text-sm">
|
||||
// <span>👁 {agent.views}</span>
|
||||
// <span>⬇️ {agent.downloads}</span>
|
||||
// </div>
|
||||
// <div className="mt-auto space-y-2">
|
||||
// <div className="flex justify-end space-x-2">
|
||||
// <Input
|
||||
// type="text"
|
||||
// placeholder="Add a comment (optional)"
|
||||
// value={comment}
|
||||
// onChange={(e) => setComment(e.target.value)}
|
||||
// />
|
||||
// {!isRejected && (
|
||||
// <form onSubmit={handleReject}>
|
||||
// <Button variant="outline" type="submit">
|
||||
// Reject
|
||||
// </Button>
|
||||
// </form>
|
||||
// )}
|
||||
// {!isApproved && (
|
||||
// <form onSubmit={handleApprove}>
|
||||
// <Button type="submit">Approve</Button>
|
||||
// </form>
|
||||
// )}
|
||||
// </div>
|
||||
// </div>
|
||||
// </Card>
|
||||
// )}
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
|
||||
export default AdminMarketplaceCard;
|
||||
// export default AdminMarketplaceCard;
|
||||
|
||||
@@ -1,114 +1,114 @@
|
||||
"use client";
|
||||
// "use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { DataTable } from "@/components/ui/data-table";
|
||||
import { Agent } from "@/lib/marketplace-api";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { ArrowUpDown } from "lucide-react";
|
||||
import { removeFeaturedAgent } from "./actions";
|
||||
import { GlobalActions } from "@/components/ui/data-table";
|
||||
// import { Button } from "@/components/ui/button";
|
||||
// import { Checkbox } from "@/components/ui/checkbox";
|
||||
// import { DataTable } from "@/components/ui/data-table";
|
||||
// import { Agent } from "@/lib/marketplace-api";
|
||||
// import { ColumnDef } from "@tanstack/react-table";
|
||||
// import { ArrowUpDown } from "lucide-react";
|
||||
// import { removeFeaturedAgent } from "./actions";
|
||||
// import { GlobalActions } from "@/components/ui/data-table";
|
||||
|
||||
export const columns: ColumnDef<Agent>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
Name
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
accessorKey: "name",
|
||||
},
|
||||
{
|
||||
header: "Description",
|
||||
accessorKey: "description",
|
||||
},
|
||||
{
|
||||
header: "Categories",
|
||||
accessorKey: "categories",
|
||||
},
|
||||
{
|
||||
header: "Keywords",
|
||||
accessorKey: "keywords",
|
||||
},
|
||||
{
|
||||
header: "Downloads",
|
||||
accessorKey: "downloads",
|
||||
},
|
||||
{
|
||||
header: "Author",
|
||||
accessorKey: "author",
|
||||
},
|
||||
{
|
||||
header: "Version",
|
||||
accessorKey: "version",
|
||||
},
|
||||
{
|
||||
header: "actions",
|
||||
cell: ({ row }) => {
|
||||
const handleRemove = async () => {
|
||||
await removeFeaturedAgentWithId();
|
||||
};
|
||||
// const handleEdit = async () => {
|
||||
// console.log("edit");
|
||||
// };
|
||||
const removeFeaturedAgentWithId = removeFeaturedAgent.bind(
|
||||
null,
|
||||
row.original.id,
|
||||
);
|
||||
return (
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" size="sm" onClick={handleRemove}>
|
||||
Remove
|
||||
</Button>
|
||||
{/* <Button variant="outline" size="sm" onClick={handleEdit}>
|
||||
Edit
|
||||
</Button> */}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
// export const columns: ColumnDef<Agent>[] = [
|
||||
// {
|
||||
// id: "select",
|
||||
// header: ({ table }) => (
|
||||
// <Checkbox
|
||||
// checked={
|
||||
// table.getIsAllPageRowsSelected() ||
|
||||
// (table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
// }
|
||||
// onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
// aria-label="Select all"
|
||||
// />
|
||||
// ),
|
||||
// cell: ({ row }) => (
|
||||
// <Checkbox
|
||||
// checked={row.getIsSelected()}
|
||||
// onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
// aria-label="Select row"
|
||||
// />
|
||||
// ),
|
||||
// },
|
||||
// {
|
||||
// header: ({ column }) => {
|
||||
// return (
|
||||
// <Button
|
||||
// variant="ghost"
|
||||
// onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
// >
|
||||
// Name
|
||||
// <ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
// </Button>
|
||||
// );
|
||||
// },
|
||||
// accessorKey: "name",
|
||||
// },
|
||||
// {
|
||||
// header: "Description",
|
||||
// accessorKey: "description",
|
||||
// },
|
||||
// {
|
||||
// header: "Categories",
|
||||
// accessorKey: "categories",
|
||||
// },
|
||||
// {
|
||||
// header: "Keywords",
|
||||
// accessorKey: "keywords",
|
||||
// },
|
||||
// {
|
||||
// header: "Downloads",
|
||||
// accessorKey: "downloads",
|
||||
// },
|
||||
// {
|
||||
// header: "Author",
|
||||
// accessorKey: "author",
|
||||
// },
|
||||
// {
|
||||
// header: "Version",
|
||||
// accessorKey: "version",
|
||||
// },
|
||||
// {
|
||||
// header: "actions",
|
||||
// cell: ({ row }) => {
|
||||
// const handleRemove = async () => {
|
||||
// await removeFeaturedAgentWithId();
|
||||
// };
|
||||
// // const handleEdit = async () => {
|
||||
// // console.log("edit");
|
||||
// // };
|
||||
// const removeFeaturedAgentWithId = removeFeaturedAgent.bind(
|
||||
// null,
|
||||
// row.original.id,
|
||||
// );
|
||||
// return (
|
||||
// <div className="flex justify-end gap-2">
|
||||
// <Button variant="outline" size="sm" onClick={handleRemove}>
|
||||
// Remove
|
||||
// </Button>
|
||||
// {/* <Button variant="outline" size="sm" onClick={handleEdit}>
|
||||
// Edit
|
||||
// </Button> */}
|
||||
// </div>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
// ];
|
||||
|
||||
export default function FeaturedAgentsTable({
|
||||
agents,
|
||||
globalActions,
|
||||
}: {
|
||||
agents: Agent[];
|
||||
globalActions: GlobalActions<Agent>[];
|
||||
}) {
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={agents}
|
||||
filterPlaceholder="Search agents..."
|
||||
filterColumn="name"
|
||||
globalActions={globalActions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// export default function FeaturedAgentsTable({
|
||||
// agents,
|
||||
// globalActions,
|
||||
// }: {
|
||||
// agents: Agent[];
|
||||
// globalActions: GlobalActions<Agent>[];
|
||||
// }) {
|
||||
// return (
|
||||
// <DataTable
|
||||
// columns={columns}
|
||||
// data={agents}
|
||||
// filterPlaceholder="Search agents..."
|
||||
// filterColumn="name"
|
||||
// globalActions={globalActions}
|
||||
// />
|
||||
// );
|
||||
// }
|
||||
|
||||
@@ -1,155 +1,155 @@
|
||||
"use server";
|
||||
import MarketplaceAPI from "@/lib/marketplace-api";
|
||||
import ServerSideMarketplaceAPI from "@/lib/marketplace-api/server-client";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { checkAuth, createServerClient } from "@/lib/supabase/server";
|
||||
import { redirect } from "next/navigation";
|
||||
import { createClient } from "@/lib/supabase/client";
|
||||
// "use server";
|
||||
// import MarketplaceAPI from "@/lib/marketplace-api";
|
||||
// import ServerSideMarketplaceAPI from "@/lib/marketplace-api/server-client";
|
||||
// import { revalidatePath } from "next/cache";
|
||||
// import * as Sentry from "@sentry/nextjs";
|
||||
// import { checkAuth, createServerClient } from "@/lib/supabase/server";
|
||||
// import { redirect } from "next/navigation";
|
||||
// import { createClient } from "@/lib/supabase/client";
|
||||
|
||||
export async function approveAgent(
|
||||
agentId: string,
|
||||
version: number,
|
||||
comment: string,
|
||||
) {
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"approveAgent",
|
||||
{},
|
||||
async () => {
|
||||
await checkAuth();
|
||||
// export async function approveAgent(
|
||||
// agentId: string,
|
||||
// version: number,
|
||||
// comment: string,
|
||||
// ) {
|
||||
// return await Sentry.withServerActionInstrumentation(
|
||||
// "approveAgent",
|
||||
// {},
|
||||
// async () => {
|
||||
// await checkAuth();
|
||||
|
||||
const api = new ServerSideMarketplaceAPI();
|
||||
await api.approveAgentSubmission(agentId, version, comment);
|
||||
console.debug(`Approving agent ${agentId}`);
|
||||
revalidatePath("/marketplace");
|
||||
},
|
||||
);
|
||||
}
|
||||
// const api = new ServerSideMarketplaceAPI();
|
||||
// await api.approveAgentSubmission(agentId, version, comment);
|
||||
// console.debug(`Approving agent ${agentId}`);
|
||||
// revalidatePath("/marketplace");
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
export async function rejectAgent(
|
||||
agentId: string,
|
||||
version: number,
|
||||
comment: string,
|
||||
) {
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"rejectAgent",
|
||||
{},
|
||||
async () => {
|
||||
await checkAuth();
|
||||
const api = new ServerSideMarketplaceAPI();
|
||||
await api.rejectAgentSubmission(agentId, version, comment);
|
||||
console.debug(`Rejecting agent ${agentId}`);
|
||||
revalidatePath("/marketplace");
|
||||
},
|
||||
);
|
||||
}
|
||||
// export async function rejectAgent(
|
||||
// agentId: string,
|
||||
// version: number,
|
||||
// comment: string,
|
||||
// ) {
|
||||
// return await Sentry.withServerActionInstrumentation(
|
||||
// "rejectAgent",
|
||||
// {},
|
||||
// async () => {
|
||||
// await checkAuth();
|
||||
// const api = new ServerSideMarketplaceAPI();
|
||||
// await api.rejectAgentSubmission(agentId, version, comment);
|
||||
// console.debug(`Rejecting agent ${agentId}`);
|
||||
// revalidatePath("/marketplace");
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
export async function getReviewableAgents() {
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"getReviewableAgents",
|
||||
{},
|
||||
async () => {
|
||||
await checkAuth();
|
||||
const api = new ServerSideMarketplaceAPI();
|
||||
return api.getAgentSubmissions();
|
||||
},
|
||||
);
|
||||
}
|
||||
// export async function getReviewableAgents() {
|
||||
// return await Sentry.withServerActionInstrumentation(
|
||||
// "getReviewableAgents",
|
||||
// {},
|
||||
// async () => {
|
||||
// await checkAuth();
|
||||
// const api = new ServerSideMarketplaceAPI();
|
||||
// return api.getAgentSubmissions();
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
export async function getFeaturedAgents(
|
||||
page: number = 1,
|
||||
pageSize: number = 10,
|
||||
) {
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"getFeaturedAgents",
|
||||
{},
|
||||
async () => {
|
||||
await checkAuth();
|
||||
const api = new ServerSideMarketplaceAPI();
|
||||
const featured = await api.getFeaturedAgents(page, pageSize);
|
||||
console.debug(`Getting featured agents ${featured.items.length}`);
|
||||
return featured;
|
||||
},
|
||||
);
|
||||
}
|
||||
// export async function getFeaturedAgents(
|
||||
// page: number = 1,
|
||||
// pageSize: number = 10,
|
||||
// ) {
|
||||
// return await Sentry.withServerActionInstrumentation(
|
||||
// "getFeaturedAgents",
|
||||
// {},
|
||||
// async () => {
|
||||
// await checkAuth();
|
||||
// const api = new ServerSideMarketplaceAPI();
|
||||
// const featured = await api.getFeaturedAgents(page, pageSize);
|
||||
// console.debug(`Getting featured agents ${featured.items.length}`);
|
||||
// return featured;
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
export async function getFeaturedAgent(agentId: string) {
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"getFeaturedAgent",
|
||||
{},
|
||||
async () => {
|
||||
await checkAuth();
|
||||
const api = new ServerSideMarketplaceAPI();
|
||||
const featured = await api.getFeaturedAgent(agentId);
|
||||
console.debug(`Getting featured agent ${featured.agentId}`);
|
||||
return featured;
|
||||
},
|
||||
);
|
||||
}
|
||||
// export async function getFeaturedAgent(agentId: string) {
|
||||
// return await Sentry.withServerActionInstrumentation(
|
||||
// "getFeaturedAgent",
|
||||
// {},
|
||||
// async () => {
|
||||
// await checkAuth();
|
||||
// const api = new ServerSideMarketplaceAPI();
|
||||
// const featured = await api.getFeaturedAgent(agentId);
|
||||
// console.debug(`Getting featured agent ${featured.agentId}`);
|
||||
// return featured;
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
export async function addFeaturedAgent(
|
||||
agentId: string,
|
||||
categories: string[] = ["featured"],
|
||||
) {
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"addFeaturedAgent",
|
||||
{},
|
||||
async () => {
|
||||
await checkAuth();
|
||||
const api = new ServerSideMarketplaceAPI();
|
||||
await api.addFeaturedAgent(agentId, categories);
|
||||
console.debug(`Adding featured agent ${agentId}`);
|
||||
revalidatePath("/marketplace");
|
||||
},
|
||||
);
|
||||
}
|
||||
// export async function addFeaturedAgent(
|
||||
// agentId: string,
|
||||
// categories: string[] = ["featured"],
|
||||
// ) {
|
||||
// return await Sentry.withServerActionInstrumentation(
|
||||
// "addFeaturedAgent",
|
||||
// {},
|
||||
// async () => {
|
||||
// await checkAuth();
|
||||
// const api = new ServerSideMarketplaceAPI();
|
||||
// await api.addFeaturedAgent(agentId, categories);
|
||||
// console.debug(`Adding featured agent ${agentId}`);
|
||||
// revalidatePath("/marketplace");
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
export async function removeFeaturedAgent(
|
||||
agentId: string,
|
||||
categories: string[] = ["featured"],
|
||||
) {
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"removeFeaturedAgent",
|
||||
{},
|
||||
async () => {
|
||||
await checkAuth();
|
||||
const api = new ServerSideMarketplaceAPI();
|
||||
await api.removeFeaturedAgent(agentId, categories);
|
||||
console.debug(`Removing featured agent ${agentId}`);
|
||||
revalidatePath("/marketplace");
|
||||
},
|
||||
);
|
||||
}
|
||||
// export async function removeFeaturedAgent(
|
||||
// agentId: string,
|
||||
// categories: string[] = ["featured"],
|
||||
// ) {
|
||||
// return await Sentry.withServerActionInstrumentation(
|
||||
// "removeFeaturedAgent",
|
||||
// {},
|
||||
// async () => {
|
||||
// await checkAuth();
|
||||
// const api = new ServerSideMarketplaceAPI();
|
||||
// await api.removeFeaturedAgent(agentId, categories);
|
||||
// console.debug(`Removing featured agent ${agentId}`);
|
||||
// revalidatePath("/marketplace");
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
export async function getCategories() {
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"getCategories",
|
||||
{},
|
||||
async () => {
|
||||
await checkAuth();
|
||||
const api = new ServerSideMarketplaceAPI();
|
||||
const categories = await api.getCategories();
|
||||
console.debug(
|
||||
`Getting categories ${categories.unique_categories.length}`,
|
||||
);
|
||||
return categories;
|
||||
},
|
||||
);
|
||||
}
|
||||
// export async function getCategories() {
|
||||
// return await Sentry.withServerActionInstrumentation(
|
||||
// "getCategories",
|
||||
// {},
|
||||
// async () => {
|
||||
// await checkAuth();
|
||||
// const api = new ServerSideMarketplaceAPI();
|
||||
// const categories = await api.getCategories();
|
||||
// console.debug(
|
||||
// `Getting categories ${categories.unique_categories.length}`,
|
||||
// );
|
||||
// return categories;
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
export async function getNotFeaturedAgents(
|
||||
page: number = 1,
|
||||
pageSize: number = 100,
|
||||
) {
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"getNotFeaturedAgents",
|
||||
{},
|
||||
async () => {
|
||||
await checkAuth();
|
||||
const api = new ServerSideMarketplaceAPI();
|
||||
const agents = await api.getNotFeaturedAgents(page, pageSize);
|
||||
console.debug(`Getting not featured agents ${agents.items.length}`);
|
||||
return agents;
|
||||
},
|
||||
);
|
||||
}
|
||||
// export async function getNotFeaturedAgents(
|
||||
// page: number = 1,
|
||||
// pageSize: number = 100,
|
||||
// ) {
|
||||
// return await Sentry.withServerActionInstrumentation(
|
||||
// "getNotFeaturedAgents",
|
||||
// {},
|
||||
// async () => {
|
||||
// await checkAuth();
|
||||
// const api = new ServerSideMarketplaceAPI();
|
||||
// const agents = await api.getNotFeaturedAgents(page, pageSize);
|
||||
// console.debug(`Getting not featured agents ${agents.items.length}`);
|
||||
// return agents;
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
@@ -96,19 +96,16 @@ export const AgentImportForm: React.FC<
|
||||
name: values.agentName,
|
||||
description: values.agentDescription,
|
||||
is_active: !values.importAsTemplate,
|
||||
is_template: values.importAsTemplate,
|
||||
};
|
||||
|
||||
(values.importAsTemplate
|
||||
? api.createTemplate(payload)
|
||||
: api.createGraph(payload)
|
||||
)
|
||||
api
|
||||
.createGraph(payload)
|
||||
.then((response) => {
|
||||
const qID = values.importAsTemplate ? "templateID" : "flowID";
|
||||
const qID = "flowID";
|
||||
window.location.href = `/build?${qID}=${response.id}`;
|
||||
})
|
||||
.catch((error) => {
|
||||
const entity_type = values.importAsTemplate ? "template" : "agent";
|
||||
const entity_type = "agent";
|
||||
form.setError("root", {
|
||||
message: `Could not create ${entity_type}: ${error}`,
|
||||
});
|
||||
@@ -159,7 +156,6 @@ export const AgentImportForm: React.FC<
|
||||
setAgentObject(agent);
|
||||
form.setValue("agentName", agent.name);
|
||||
form.setValue("agentDescription", agent.description);
|
||||
form.setValue("importAsTemplate", agent.is_template);
|
||||
} catch (error) {
|
||||
console.error("Error loading agent file:", error);
|
||||
}
|
||||
@@ -202,41 +198,6 @@ export const AgentImportForm: React.FC<
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="importAsTemplate"
|
||||
disabled={!agentObject}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Import as</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span
|
||||
className={
|
||||
field.value ? "text-gray-400 dark:text-gray-600" : ""
|
||||
}
|
||||
>
|
||||
Agent
|
||||
</span>
|
||||
<Switch
|
||||
data-testid="import-as-template-switch"
|
||||
disabled={field.disabled}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
<span
|
||||
className={
|
||||
field.value ? "" : "text-gray-400 dark:text-gray-600"
|
||||
}
|
||||
>
|
||||
Template
|
||||
</span>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import * as React from "react";
|
||||
import Image from "next/image";
|
||||
import { PlayIcon } from "@radix-ui/react-icons";
|
||||
import { Button } from "./Button";
|
||||
|
||||
const isValidVideoFile = (url: string): boolean => {
|
||||
const videoExtensions = /\.(mp4|webm|ogg)$/i;
|
||||
return videoExtensions.test(url);
|
||||
};
|
||||
|
||||
const isValidVideoUrl = (url: string): boolean => {
|
||||
const videoExtensions = /\.(mp4|webm|ogg)$/i;
|
||||
const youtubeRegex = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/;
|
||||
return videoExtensions.test(url) || youtubeRegex.test(url);
|
||||
};
|
||||
|
||||
const getYouTubeVideoId = (url: string) => {
|
||||
const regExp =
|
||||
/^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
|
||||
const match = url.match(regExp);
|
||||
return match && match[7].length === 11 ? match[7] : null;
|
||||
};
|
||||
|
||||
interface AgentImageItemProps {
|
||||
image: string;
|
||||
index: number;
|
||||
playingVideoIndex: number | null;
|
||||
handlePlay: (index: number) => void;
|
||||
handlePause: (index: number) => void;
|
||||
}
|
||||
|
||||
export const AgentImageItem: React.FC<AgentImageItemProps> = React.memo(
|
||||
({ image, index, playingVideoIndex, handlePlay, handlePause }) => {
|
||||
const videoRef = React.useRef<HTMLVideoElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
playingVideoIndex !== index &&
|
||||
videoRef.current &&
|
||||
!videoRef.current.paused
|
||||
) {
|
||||
videoRef.current.pause();
|
||||
}
|
||||
}, [playingVideoIndex, index]);
|
||||
|
||||
const isVideoFile = isValidVideoFile(image);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="h-[15rem] overflow-hidden rounded-xl bg-[#a8a8a8] dark:bg-neutral-700 sm:h-[20rem] sm:w-full md:h-[25rem] lg:h-[30rem]">
|
||||
{isValidVideoUrl(image) ? (
|
||||
getYouTubeVideoId(image) ? (
|
||||
<iframe
|
||||
width="100%"
|
||||
height="100%"
|
||||
src={`https://www.youtube.com/embed/${getYouTubeVideoId(image)}`}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
title="YouTube video player"
|
||||
></iframe>
|
||||
) : (
|
||||
<div className="relative h-full w-full overflow-hidden">
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
controls
|
||||
preload="metadata"
|
||||
poster={`${image}#t=0.1`}
|
||||
style={{ objectPosition: "center 25%" }}
|
||||
onPlay={() => handlePlay(index)}
|
||||
onPause={() => handlePause(index)}
|
||||
autoPlay={false}
|
||||
title="Video"
|
||||
>
|
||||
<source src={image} type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="relative h-full w-full">
|
||||
<Image
|
||||
src={image}
|
||||
alt="Image"
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
className="rounded-xl object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isVideoFile && playingVideoIndex !== index && (
|
||||
<div className="absolute bottom-2 left-2 sm:bottom-3 sm:left-3 md:bottom-4 md:left-4 lg:bottom-[1.25rem] lg:left-[1.25rem]">
|
||||
<Button
|
||||
variant="default"
|
||||
size="default"
|
||||
onClick={() => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.play();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="pr-1 font-neue text-sm font-medium leading-6 tracking-tight text-[#272727] dark:text-neutral-200 sm:pr-2 sm:text-base sm:leading-7 md:text-lg md:leading-8 lg:text-xl lg:leading-9">
|
||||
Play demo
|
||||
</span>
|
||||
<PlayIcon className="h-5 w-5 text-black dark:text-neutral-200 sm:h-6 sm:w-6 md:h-7 md:w-7" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
AgentImageItem.displayName = "AgentImageItem";
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { AgentImages } from "./AgentImages";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Agent Images",
|
||||
component: AgentImages,
|
||||
parameters: {
|
||||
layout: {
|
||||
center: true,
|
||||
fullscreen: true,
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
images: { control: "object" },
|
||||
},
|
||||
} satisfies Meta<typeof AgentImages>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
images: [
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
"https://youtu.be/KWonAsyKF3g?si=JMibxlN_6OVo6LhJ",
|
||||
"https://storage.googleapis.com/agpt-dev-website-media/DJINeo.mp4",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const OnlyImages: Story = {
|
||||
args: {
|
||||
images: [
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
"https://upload.wikimedia.org/wikipedia/commons/c/c5/Big_buck_bunny_poster_big.jpg",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithVideos: Story = {
|
||||
args: {
|
||||
images: [
|
||||
"https://storage.googleapis.com/agpt-dev-website-media/DJINeo.mp4",
|
||||
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
|
||||
"https://youtu.be/KWonAsyKF3g?si=JMibxlN_6OVo6LhJ",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const SingleItem: Story = {
|
||||
args: {
|
||||
images: [
|
||||
"https://upload.wikimedia.org/wikipedia/commons/c/c5/Big_buck_bunny_poster_big.jpg",
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { AgentImageItem } from "./AgentImageItem";
|
||||
|
||||
interface AgentImagesProps {
|
||||
images: string[];
|
||||
}
|
||||
|
||||
export const AgentImages: React.FC<AgentImagesProps> = ({ images }) => {
|
||||
const [playingVideoIndex, setPlayingVideoIndex] = React.useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const handlePlay = React.useCallback((index: number) => {
|
||||
setPlayingVideoIndex(index);
|
||||
}, []);
|
||||
|
||||
const handlePause = React.useCallback(
|
||||
(index: number) => {
|
||||
if (playingVideoIndex === index) {
|
||||
setPlayingVideoIndex(null);
|
||||
}
|
||||
},
|
||||
[playingVideoIndex],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-y-auto bg-white px-2 dark:bg-gray-800 lg:w-[56.25rem]">
|
||||
<div className="space-y-4 sm:space-y-6 md:space-y-[1.875rem]">
|
||||
{images.map((image, index) => (
|
||||
<AgentImageItem
|
||||
key={index}
|
||||
image={image}
|
||||
index={index}
|
||||
playingVideoIndex={playingVideoIndex}
|
||||
handlePlay={handlePlay}
|
||||
handlePause={handlePause}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,136 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { AgentInfo } from "./AgentInfo";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Agent Info",
|
||||
component: AgentInfo,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
onRunAgent: { action: "run agent clicked" },
|
||||
name: { control: "text" },
|
||||
creator: { control: "text" },
|
||||
shortDescription: { control: "text" },
|
||||
longDescription: { control: "text" },
|
||||
rating: { control: "number", min: 0, max: 5, step: 0.1 },
|
||||
runs: { control: "number" },
|
||||
categories: { control: "object" },
|
||||
lastUpdated: { control: "text" },
|
||||
version: { control: "text" },
|
||||
},
|
||||
} satisfies Meta<typeof AgentInfo>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
onRunAgent: () => console.log("Run agent clicked"),
|
||||
name: "AI Video Generator",
|
||||
creator: "Toran Richards",
|
||||
shortDescription:
|
||||
"Transform ideas into breathtaking images with this AI-powered Image Generator.",
|
||||
longDescription: `Create Viral-Ready Content in Seconds! Transform trending topics into engaging videos with this cutting-edge AI Video Generator. Perfect for content creators, social media managers, and marketers looking to quickly produce high-quality content.
|
||||
|
||||
Key features include:
|
||||
- Customizable video output
|
||||
- 15+ pre-made templates
|
||||
- Auto scene detection
|
||||
- Smart text-to-speech
|
||||
- Multiple export formats
|
||||
- SEO-optimized suggestions`,
|
||||
rating: 4.7,
|
||||
runs: 1500,
|
||||
categories: ["Video", "Content Creation", "Social Media"],
|
||||
lastUpdated: "2 days ago",
|
||||
version: "1.2.0",
|
||||
},
|
||||
};
|
||||
|
||||
export const LowRating: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
name: "Data Analyzer",
|
||||
creator: "DataTech",
|
||||
shortDescription:
|
||||
"Analyze complex datasets with machine learning algorithms",
|
||||
longDescription:
|
||||
"A comprehensive data analysis tool that leverages machine learning to provide deep insights into your datasets. Currently in beta testing phase.",
|
||||
rating: 2.7,
|
||||
runs: 5000,
|
||||
categories: ["Data Analysis", "Machine Learning"],
|
||||
lastUpdated: "1 week ago",
|
||||
version: "0.9.5",
|
||||
},
|
||||
};
|
||||
|
||||
export const HighRuns: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
name: "Code Assistant",
|
||||
creator: "DevAI",
|
||||
shortDescription:
|
||||
"Get AI-powered coding help for various programming languages",
|
||||
longDescription:
|
||||
"An advanced AI coding assistant that supports multiple programming languages and frameworks. Features include code completion, refactoring suggestions, and bug detection.",
|
||||
rating: 4.8,
|
||||
runs: 1000000,
|
||||
categories: ["Programming", "AI", "Developer Tools"],
|
||||
lastUpdated: "1 day ago",
|
||||
version: "2.1.3",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInteraction: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
name: "Task Planner",
|
||||
creator: "Productivity AI",
|
||||
shortDescription: "Plan and organize your tasks efficiently with AI",
|
||||
longDescription:
|
||||
"An intelligent task management system that helps you organize, prioritize, and complete your tasks more efficiently. Features smart scheduling and AI-powered suggestions.",
|
||||
rating: 4.2,
|
||||
runs: 50000,
|
||||
categories: ["Productivity", "Task Management", "AI"],
|
||||
lastUpdated: "3 days ago",
|
||||
version: "1.5.2",
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Test run agent button
|
||||
const runButton = canvas.getByText("Run agent");
|
||||
await userEvent.hover(runButton);
|
||||
await userEvent.click(runButton);
|
||||
|
||||
// Test rating interaction
|
||||
const ratingStars = canvas.getAllByLabelText(/Star Icon/);
|
||||
await userEvent.hover(ratingStars[3]);
|
||||
await userEvent.click(ratingStars[3]);
|
||||
|
||||
// Test category interaction
|
||||
const category = canvas.getByText("Productivity");
|
||||
await userEvent.hover(category);
|
||||
await userEvent.click(category);
|
||||
},
|
||||
};
|
||||
|
||||
export const LongDescription: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
name: "AI Writing Assistant",
|
||||
creator: "WordCraft AI",
|
||||
shortDescription:
|
||||
"Enhance your writing with our advanced AI-powered assistant.",
|
||||
longDescription:
|
||||
"It offers real-time suggestions for grammar, style, and tone, helps with research and fact-checking, and can even generate content ideas based on your input.",
|
||||
rating: 4.7,
|
||||
runs: 75000,
|
||||
categories: ["Writing", "AI", "Content Creation"],
|
||||
lastUpdated: "5 days ago",
|
||||
version: "3.0.1",
|
||||
},
|
||||
};
|
||||
120
autogpt_platform/frontend/src/components/agptui/AgentInfo.tsx
Normal file
120
autogpt_platform/frontend/src/components/agptui/AgentInfo.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { IconPlay, IconStar, StarRatingIcons } from "@/components/ui/icons";
|
||||
import Link from "next/link";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
interface AgentInfoProps {
|
||||
name: string;
|
||||
creator: string;
|
||||
shortDescription: string;
|
||||
longDescription: string;
|
||||
rating: number;
|
||||
runs: number;
|
||||
categories: string[];
|
||||
lastUpdated: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export const AgentInfo: React.FC<AgentInfoProps> = ({
|
||||
name,
|
||||
creator,
|
||||
shortDescription,
|
||||
longDescription,
|
||||
rating,
|
||||
runs,
|
||||
categories,
|
||||
lastUpdated,
|
||||
version,
|
||||
}) => {
|
||||
return (
|
||||
<div className="w-full max-w-[396px] px-4 sm:px-6 lg:w-[396px] lg:px-0">
|
||||
{/* Title */}
|
||||
<div className="font-poppins mb-3 w-full text-2xl font-medium leading-normal text-neutral-900 dark:text-neutral-100 sm:text-3xl lg:mb-4 lg:text-[35px] lg:leading-10">
|
||||
{name}
|
||||
</div>
|
||||
|
||||
{/* Creator */}
|
||||
<div className="mb-3 flex w-full items-center gap-1.5 lg:mb-4">
|
||||
<div className="font-geist text-base font-normal text-neutral-800 dark:text-neutral-200 sm:text-lg lg:text-xl">
|
||||
by
|
||||
</div>
|
||||
<div className="font-geist text-base font-medium text-neutral-800 dark:text-neutral-200 sm:text-lg lg:text-xl">
|
||||
{creator}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Short Description */}
|
||||
<div className="font-geist mb-4 line-clamp-2 w-full text-base font-normal leading-normal text-neutral-600 dark:text-neutral-300 sm:text-lg lg:mb-6 lg:text-xl lg:leading-7">
|
||||
{shortDescription}
|
||||
</div>
|
||||
|
||||
{/* Run Agent Button */}
|
||||
<div className="mb-4 w-full lg:mb-6">
|
||||
<button className="inline-flex w-full items-center justify-center gap-2 rounded-[38px] bg-violet-600 px-4 py-3 transition-colors hover:bg-violet-700 sm:w-auto sm:gap-2.5 sm:px-5 sm:py-3.5 lg:px-6 lg:py-4">
|
||||
<IconPlay className="h-5 w-5 text-white sm:h-5 sm:w-5 lg:h-6 lg:w-6" />
|
||||
<span className="font-poppins text-base font-medium text-neutral-50 sm:text-lg">
|
||||
Run agent
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Rating and Runs */}
|
||||
<div className="mb-4 flex w-full items-center justify-between lg:mb-6">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2">
|
||||
<span className="font-geist whitespace-nowrap text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
|
||||
{rating.toFixed(1)}
|
||||
</span>
|
||||
<div className="flex gap-0.5">{StarRatingIcons(rating)}</div>
|
||||
</div>
|
||||
<div className="font-geist whitespace-nowrap text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
|
||||
{runs.toLocaleString()} runs
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<Separator className="mb-4 lg:mb-6" />
|
||||
|
||||
{/* Description Section */}
|
||||
<div className="mb-4 w-full lg:mb-6">
|
||||
<div className="mb-1.5 text-xs font-medium text-neutral-800 dark:text-neutral-200 sm:mb-2 sm:text-sm">
|
||||
Description
|
||||
</div>
|
||||
<div className="font-geist w-full whitespace-pre-line text-sm font-normal text-neutral-600 dark:text-neutral-300 sm:text-base">
|
||||
{longDescription}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="mb-4 flex w-full flex-col gap-1.5 sm:gap-2 lg:mb-6">
|
||||
<div className="text-xs font-medium text-neutral-800 dark:text-neutral-200 sm:text-sm">
|
||||
Categories
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5 sm:gap-2">
|
||||
{categories.map((category, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="whitespace-nowrap rounded-full border border-neutral-200 bg-white px-2 py-0.5 text-xs text-neutral-800 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-200 sm:px-3 sm:py-1 sm:text-sm"
|
||||
>
|
||||
{category}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Version History */}
|
||||
<div className="flex w-full flex-col gap-0.5 sm:gap-1">
|
||||
<div className="text-xs font-medium text-neutral-800 dark:text-neutral-200 sm:text-sm">
|
||||
Version history
|
||||
</div>
|
||||
<div className="text-xs text-neutral-600 dark:text-neutral-400 sm:text-sm">
|
||||
Last updated {lastUpdated}
|
||||
</div>
|
||||
<div className="text-xs text-neutral-600 dark:text-neutral-400 sm:text-sm">
|
||||
Version {version}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { AgentTable } from "./AgentTable";
|
||||
import { AgentTableRowProps } from "./AgentTableRow";
|
||||
import { userEvent, within, expect } from "@storybook/test";
|
||||
import { StatusType } from "./Status";
|
||||
|
||||
const meta: Meta<typeof AgentTable> = {
|
||||
title: "AGPT UI/Agent Table",
|
||||
component: AgentTable,
|
||||
tags: ["autodocs"],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AgentTable>;
|
||||
|
||||
const sampleAgents: AgentTableRowProps[] = [
|
||||
{
|
||||
id: "agent-1",
|
||||
agentName: "Super Coder",
|
||||
description: "An AI agent that writes clean, efficient code",
|
||||
imageSrc:
|
||||
"https://ddz4ak4pa3d19.cloudfront.net/cache/53/b2/53b2bc7d7900f0e1e60bf64ebf38032d.jpg",
|
||||
dateSubmitted: "2023-05-15",
|
||||
status: "approved",
|
||||
runs: 1500,
|
||||
rating: 4.8,
|
||||
onEdit: () => console.log("Edit Super Coder"),
|
||||
},
|
||||
{
|
||||
id: "agent-2",
|
||||
agentName: "Data Analyzer",
|
||||
description: "Processes and analyzes large datasets with ease",
|
||||
imageSrc:
|
||||
"https://ddz4ak4pa3d19.cloudfront.net/cache/40/f7/40f7bc97c952f8df0f9c88d29defe8d4.jpg",
|
||||
dateSubmitted: "2023-05-10",
|
||||
status: "awaiting_review",
|
||||
runs: 1200,
|
||||
rating: 4.5,
|
||||
onEdit: () => console.log("Edit Data Analyzer"),
|
||||
},
|
||||
{
|
||||
id: "agent-3",
|
||||
agentName: "UI Designer",
|
||||
description: "Creates beautiful and intuitive user interfaces",
|
||||
imageSrc:
|
||||
"https://ddz4ak4pa3d19.cloudfront.net/cache/14/9e/149ebb9014aa8c0097e72ed89845af0e.jpg",
|
||||
dateSubmitted: "2023-05-05",
|
||||
status: "draft",
|
||||
runs: 800,
|
||||
rating: 4.2,
|
||||
onEdit: () => console.log("Edit UI Designer"),
|
||||
},
|
||||
];
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
agents: sampleAgents,
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyTable: Story = {
|
||||
args: {
|
||||
agents: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Tests
|
||||
export const InteractionTest: Story = {
|
||||
...Default,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const editButtons = await canvas.findAllByText("Edit");
|
||||
await userEvent.click(editButtons[0]);
|
||||
// You would typically assert something here, but console.log is used in the mocked function
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyTableTest: Story = {
|
||||
...EmptyTable,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const emptyMessage = canvas.getByText("No agents found");
|
||||
expect(emptyMessage).toBeTruthy();
|
||||
},
|
||||
};
|
||||
107
autogpt_platform/frontend/src/components/agptui/AgentTable.tsx
Normal file
107
autogpt_platform/frontend/src/components/agptui/AgentTable.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { AgentTableRow, AgentTableRowProps } from "./AgentTableRow";
|
||||
import { AgentTableCard } from "./AgentTableCard";
|
||||
import { StoreSubmissionRequest } from "@/lib/autogpt-server-api/types";
|
||||
|
||||
export interface AgentTableProps {
|
||||
agents: AgentTableRowProps[];
|
||||
onEditSubmission: (submission: StoreSubmissionRequest) => void;
|
||||
onDeleteSubmission: (submission_id: string) => void;
|
||||
}
|
||||
|
||||
export const AgentTable: React.FC<AgentTableProps> = ({
|
||||
agents,
|
||||
onEditSubmission,
|
||||
onDeleteSubmission,
|
||||
}) => {
|
||||
// Use state to track selected agents
|
||||
const [selectedAgents, setSelectedAgents] = React.useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
|
||||
// Handle select all checkbox
|
||||
const handleSelectAll = React.useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedAgents(new Set(agents.map((agent) => agent.id.toString())));
|
||||
} else {
|
||||
setSelectedAgents(new Set());
|
||||
}
|
||||
},
|
||||
[agents],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* Table header - Hide on mobile */}
|
||||
<div className="hidden flex-col md:flex">
|
||||
<div className="border-t border-neutral-300 dark:border-neutral-700" />
|
||||
<div className="flex items-center px-4 py-2">
|
||||
<div className="flex items-center">
|
||||
<div className="flex min-w-[120px] items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="selectAllAgents"
|
||||
aria-label="Select all agents"
|
||||
className="mr-4 h-5 w-5 rounded border-2 border-neutral-400 dark:border-neutral-600"
|
||||
checked={
|
||||
selectedAgents.size === agents.length && agents.length > 0
|
||||
}
|
||||
onChange={handleSelectAll}
|
||||
/>
|
||||
<label
|
||||
htmlFor="selectAllAgents"
|
||||
className="text-sm font-medium text-neutral-800 dark:text-neutral-200"
|
||||
>
|
||||
Select all
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-2 grid w-full grid-cols-[400px,150px,150px,100px,100px,50px] items-center">
|
||||
<div className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
|
||||
Agent info
|
||||
</div>
|
||||
<div className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
|
||||
Date submitted
|
||||
</div>
|
||||
<div className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
|
||||
Status
|
||||
</div>
|
||||
<div className="text-right text-sm font-medium text-neutral-800 dark:text-neutral-200">
|
||||
Runs
|
||||
</div>
|
||||
<div className="text-right text-sm font-medium text-neutral-800 dark:text-neutral-200">
|
||||
Reviews
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-b border-neutral-300 dark:border-neutral-700" />
|
||||
</div>
|
||||
|
||||
{/* Table body */}
|
||||
{agents.length > 0 ? (
|
||||
<div className="flex flex-col">
|
||||
{agents.map((agent, index) => (
|
||||
<div key={agent.id} className="md:block">
|
||||
<AgentTableRow
|
||||
{...agent}
|
||||
onEditSubmission={onEditSubmission}
|
||||
onDeleteSubmission={onDeleteSubmission}
|
||||
/>
|
||||
<div className="block md:hidden">
|
||||
<AgentTableCard {...agent} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-4 text-center font-['Geist'] text-base text-neutral-600 dark:text-neutral-400">
|
||||
No agents available. Create your first agent to get started!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { AgentTableCard } from "./AgentTableCard";
|
||||
import { userEvent, within, expect } from "@storybook/test";
|
||||
import { type StatusType } from "./Status";
|
||||
|
||||
const meta: Meta<typeof AgentTableCard> = {
|
||||
title: "AGPT UI/Agent Table Card",
|
||||
component: AgentTableCard,
|
||||
tags: ["autodocs"],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AgentTableCard>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
agentName: "Super Coder",
|
||||
description: "An AI agent that writes clean, efficient code",
|
||||
imageSrc:
|
||||
"https://ddz4ak4pa3d19.cloudfront.net/cache/53/b2/53b2bc7d7900f0e1e60bf64ebf38032d.jpg",
|
||||
dateSubmitted: "2023-05-15",
|
||||
status: "ACTIVE" as StatusType,
|
||||
runs: 1500,
|
||||
rating: 4.8,
|
||||
onEdit: () => console.log("Edit Super Coder"),
|
||||
},
|
||||
};
|
||||
|
||||
export const NoRating: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
rating: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const NoRuns: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
runs: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const InactiveAgent: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
status: "INACTIVE" as StatusType,
|
||||
},
|
||||
};
|
||||
|
||||
export const LongDescription: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
description:
|
||||
"This is a very long description that should wrap to multiple lines. It contains detailed information about the agent and its capabilities.",
|
||||
},
|
||||
};
|
||||
|
||||
export const InteractionTest: Story = {
|
||||
...Default,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const moreButton = canvas.getByRole("button");
|
||||
await userEvent.click(moreButton);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import Image from "next/image";
|
||||
import { IconStarFilled, IconMore } from "@/components/ui/icons";
|
||||
import { Status, StatusType } from "./Status";
|
||||
|
||||
export interface AgentTableCardProps {
|
||||
agent_id: string;
|
||||
agent_version: number;
|
||||
agentName: string;
|
||||
sub_heading: string;
|
||||
description: string;
|
||||
imageSrc: string[];
|
||||
dateSubmitted: string;
|
||||
status: StatusType;
|
||||
runs: number;
|
||||
rating: number;
|
||||
id: number;
|
||||
onEditSubmission: (submission: StoreSubmissionRequest) => void;
|
||||
}
|
||||
|
||||
export const AgentTableCard: React.FC<AgentTableCardProps> = ({
|
||||
agent_id,
|
||||
agent_version,
|
||||
agentName,
|
||||
sub_heading,
|
||||
description,
|
||||
imageSrc,
|
||||
dateSubmitted,
|
||||
status,
|
||||
runs,
|
||||
rating,
|
||||
id,
|
||||
onEditSubmission,
|
||||
}) => {
|
||||
const onEdit = () => {
|
||||
console.log("Edit agent", agentName);
|
||||
onEditSubmission({
|
||||
agent_id,
|
||||
agent_version,
|
||||
slug: "",
|
||||
name: agentName,
|
||||
sub_heading,
|
||||
description,
|
||||
image_urls: imageSrc,
|
||||
categories: [],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-b border-neutral-300 p-4 dark:border-neutral-700">
|
||||
<div className="flex gap-4">
|
||||
<div className="relative h-[56px] w-[100px] overflow-hidden rounded-lg bg-[#d9d9d9] dark:bg-neutral-800">
|
||||
<Image
|
||||
src={imageSrc?.[0] ?? "/nada.png"}
|
||||
alt={agentName}
|
||||
fill
|
||||
style={{ objectFit: "cover" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-[15px] font-medium text-neutral-800 dark:text-neutral-200">
|
||||
{agentName}
|
||||
</h3>
|
||||
<p className="line-clamp-2 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="h-fit rounded-full p-1 hover:bg-neutral-100 dark:hover:bg-neutral-700"
|
||||
>
|
||||
<IconMore className="h-5 w-5 text-neutral-800 dark:text-neutral-200" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-4">
|
||||
<Status status={status} />
|
||||
<div className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{dateSubmitted}
|
||||
</div>
|
||||
<div className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{runs.toLocaleString()} runs
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
|
||||
{rating.toFixed(1)}
|
||||
</span>
|
||||
<IconStarFilled className="h-4 w-4 text-neutral-800 dark:text-neutral-200" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,170 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import Image from "next/image";
|
||||
import { IconStarFilled, IconMore, IconEdit } from "@/components/ui/icons";
|
||||
import { Status, StatusType } from "./Status";
|
||||
import * as ContextMenu from "@radix-ui/react-context-menu";
|
||||
import { TrashIcon } from "@radix-ui/react-icons";
|
||||
import { StoreSubmissionRequest } from "@/lib/autogpt-server-api/types";
|
||||
|
||||
export interface AgentTableRowProps {
|
||||
agent_id: string;
|
||||
agent_version: number;
|
||||
agentName: string;
|
||||
sub_heading: string;
|
||||
description: string;
|
||||
imageSrc: string[];
|
||||
date_submitted: string;
|
||||
status: StatusType;
|
||||
runs: number;
|
||||
rating: number;
|
||||
dateSubmitted: string;
|
||||
id: number;
|
||||
onEditSubmission: (submission: StoreSubmissionRequest) => void;
|
||||
onDeleteSubmission: (submission_id: string) => void;
|
||||
}
|
||||
|
||||
export const AgentTableRow: React.FC<AgentTableRowProps> = ({
|
||||
agent_id,
|
||||
agent_version,
|
||||
agentName,
|
||||
sub_heading,
|
||||
description,
|
||||
imageSrc,
|
||||
dateSubmitted,
|
||||
status,
|
||||
runs,
|
||||
rating,
|
||||
id,
|
||||
onEditSubmission,
|
||||
onDeleteSubmission,
|
||||
}) => {
|
||||
// Create a unique ID for the checkbox
|
||||
const checkboxId = `agent-${id}-checkbox`;
|
||||
|
||||
const handleEdit = React.useCallback(() => {
|
||||
onEditSubmission({
|
||||
agent_id,
|
||||
agent_version,
|
||||
slug: "",
|
||||
name: agentName,
|
||||
sub_heading,
|
||||
description,
|
||||
image_urls: imageSrc,
|
||||
categories: [],
|
||||
} as StoreSubmissionRequest);
|
||||
}, [
|
||||
agent_id,
|
||||
agent_version,
|
||||
agentName,
|
||||
sub_heading,
|
||||
description,
|
||||
imageSrc,
|
||||
onEditSubmission,
|
||||
]);
|
||||
|
||||
const handleDelete = React.useCallback(() => {
|
||||
onDeleteSubmission(agent_id);
|
||||
}, [agent_id, onDeleteSubmission]);
|
||||
|
||||
return (
|
||||
<div className="hidden items-center border-b border-neutral-300 px-4 py-4 hover:bg-neutral-50 dark:border-neutral-700 dark:hover:bg-neutral-800 md:flex">
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={checkboxId}
|
||||
aria-label={`Select ${agentName}`}
|
||||
className="mr-4 h-5 w-5 rounded border-2 border-neutral-400 dark:border-neutral-600"
|
||||
/>
|
||||
{/* Single label instead of multiple */}
|
||||
<label htmlFor={checkboxId} className="sr-only">
|
||||
Select {agentName}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid w-full grid-cols-[minmax(400px,1fr),180px,140px,100px,100px,40px] items-center gap-4">
|
||||
{/* Agent info column */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative h-[70px] w-[125px] overflow-hidden rounded-[10px] bg-[#d9d9d9] dark:bg-neutral-700">
|
||||
<Image
|
||||
src={imageSrc?.[0] ?? "/nada.png"}
|
||||
alt={agentName}
|
||||
fill
|
||||
style={{ objectFit: "cover" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-[15px] font-medium text-neutral-800 dark:text-neutral-200">
|
||||
{agentName}
|
||||
</h3>
|
||||
<p className="line-clamp-2 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date column */}
|
||||
<div className="pl-14 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{dateSubmitted}
|
||||
</div>
|
||||
|
||||
{/* Status column */}
|
||||
<div>
|
||||
<Status status={status} />
|
||||
</div>
|
||||
|
||||
{/* Runs column */}
|
||||
<div className="text-right text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{runs?.toLocaleString() ?? "0"}
|
||||
</div>
|
||||
|
||||
{/* Reviews column */}
|
||||
<div className="text-right">
|
||||
{rating ? (
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
|
||||
{rating.toFixed(1)}
|
||||
</span>
|
||||
<IconStarFilled className="h-4 w-4 text-neutral-800 dark:text-neutral-200" />
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
No reviews
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions - Three dots menu */}
|
||||
<div className="flex justify-end">
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger>
|
||||
<button className="rounded-full p-1 hover:bg-neutral-100 dark:hover:bg-neutral-700">
|
||||
<IconMore className="h-5 w-5 text-neutral-800 dark:text-neutral-200" />
|
||||
</button>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Content className="z-10 rounded-xl border bg-white p-1 shadow-md dark:bg-gray-800">
|
||||
<ContextMenu.Item
|
||||
onSelect={handleEdit}
|
||||
className="flex cursor-pointer items-center rounded-md px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<IconEdit className="mr-2 h-5 w-5 dark:text-gray-100" />
|
||||
<span className="dark:text-gray-100">Edit</span>
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Separator className="my-1 h-px bg-gray-300 dark:bg-gray-600" />
|
||||
<ContextMenu.Item
|
||||
onSelect={handleDelete}
|
||||
className="flex cursor-pointer items-center rounded-md px-3 py-2 text-red-500 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<TrashIcon className="mr-2 h-5 w-5 text-red-500 dark:text-red-400" />
|
||||
<span className="dark:text-red-400">Delete</span>
|
||||
</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Root>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { BecomeACreator } from "./BecomeACreator";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Become A Creator",
|
||||
component: BecomeACreator,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
title: { control: "text" },
|
||||
heading: { control: "text" },
|
||||
description: { control: "text" },
|
||||
buttonText: { control: "text" },
|
||||
onButtonClick: { action: "buttonClicked" },
|
||||
},
|
||||
} satisfies Meta<typeof BecomeACreator>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
title: "Want to contribute?",
|
||||
heading: "We're always looking for more Creators!",
|
||||
description: "Join our ever-growing community of hackers and tinkerers",
|
||||
buttonText: "Become a Creator",
|
||||
onButtonClick: () => console.log("Button clicked"),
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomText: Story = {
|
||||
args: {
|
||||
title: "Become a Creator Today!",
|
||||
heading: "Join Our Creator Community",
|
||||
description: "Share your ideas and build amazing AI agents with us",
|
||||
buttonText: "Start Creating",
|
||||
onButtonClick: () => console.log("Custom button clicked"),
|
||||
},
|
||||
};
|
||||
|
||||
export const LongDescription: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
description:
|
||||
"Join our vibrant community of innovators, developers, and AI enthusiasts. Share your unique perspectives, collaborate on groundbreaking projects, and help shape the future of AI technology.",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInteraction: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const button = canvas.getByText("Become a Creator");
|
||||
|
||||
await userEvent.click(button);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { PublishAgentPopout } from "./composite/PublishAgentPopout";
|
||||
interface BecomeACreatorProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
buttonText?: string;
|
||||
onButtonClick?: () => void;
|
||||
}
|
||||
|
||||
export const BecomeACreator: React.FC<BecomeACreatorProps> = ({
|
||||
title = "Become a creator",
|
||||
description = "Join a community where your AI creations can inspire, engage, and be downloaded by users around the world.",
|
||||
buttonText = "Upload your agent",
|
||||
onButtonClick,
|
||||
}) => {
|
||||
const handleButtonClick = () => {
|
||||
onButtonClick?.();
|
||||
console.log("Become A Creator clicked");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative mx-auto h-auto min-h-[300px] w-full max-w-[1360px] md:min-h-[400px] lg:h-[459px]">
|
||||
{/* Top border */}
|
||||
<div className="left-0 top-0 h-px w-full bg-gray-200 dark:bg-gray-700" />
|
||||
|
||||
{/* Title */}
|
||||
<h2 className="mb-8 mt-6 text-2xl leading-7 text-neutral-800 dark:text-neutral-200">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
{/* Content Container */}
|
||||
<div className="absolute left-1/2 top-1/2 w-full max-w-[900px] -translate-x-1/2 -translate-y-1/2 px-4 pt-16 text-center md:px-6 md:pt-10 lg:px-0">
|
||||
<h2 className="font-poppins mb-6 text-3xl font-semibold leading-tight text-neutral-950 dark:text-neutral-50 md:mb-8 md:text-4xl md:leading-[1.2] lg:mb-12 lg:text-5xl lg:leading-[54px]">
|
||||
Build AI agents and share
|
||||
<br />
|
||||
<span className="text-violet-600 dark:text-violet-400">
|
||||
your
|
||||
</span>{" "}
|
||||
vision
|
||||
</h2>
|
||||
|
||||
<p className="font-geist mx-auto mb-8 max-w-[90%] text-lg font-normal leading-relaxed text-neutral-700 dark:text-neutral-300 md:mb-10 md:text-xl md:leading-loose lg:mb-14 lg:text-2xl">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
<PublishAgentPopout
|
||||
trigger={
|
||||
<button
|
||||
onClick={handleButtonClick}
|
||||
className="inline-flex h-[48px] cursor-pointer items-center justify-center rounded-[38px] bg-neutral-800 px-8 py-3 transition-colors hover:bg-neutral-700 dark:bg-neutral-700 dark:hover:bg-neutral-600 md:h-[56px] md:px-10 md:py-4 lg:h-[68px] lg:px-12 lg:py-5"
|
||||
>
|
||||
<span className="font-poppins whitespace-nowrap text-base font-medium leading-normal text-neutral-50 md:text-lg md:leading-relaxed lg:text-xl lg:leading-7">
|
||||
{buttonText}
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { BreadCrumbs } from "./BreadCrumbs";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/BreadCrumbs",
|
||||
component: BreadCrumbs,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
items: { control: "object" },
|
||||
},
|
||||
} satisfies Meta<typeof BreadCrumbs>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{ name: "Home", link: "/" },
|
||||
{ name: "Agents", link: "/agents" },
|
||||
{ name: "SEO Optimizer", link: "/agents/seo-optimizer" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const SingleItem: Story = {
|
||||
args: {
|
||||
items: [{ name: "Home", link: "/" }],
|
||||
},
|
||||
};
|
||||
|
||||
export const LongPath: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{ name: "Home", link: "/" },
|
||||
{ name: "Categories", link: "/categories" },
|
||||
{ name: "AI Tools", link: "/categories/ai-tools" },
|
||||
{ name: "Data Analysis", link: "/categories/ai-tools/data-analysis" },
|
||||
{
|
||||
name: "Data Analyzer",
|
||||
link: "/categories/ai-tools/data-analysis/data-analyzer",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInteraction: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{ name: "Home", link: "/" },
|
||||
{ name: "Agents", link: "/agents" },
|
||||
{ name: "Task Planner", link: "/agents/task-planner" },
|
||||
],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const homeLink = canvas.getByText("Home");
|
||||
|
||||
await userEvent.hover(homeLink);
|
||||
await userEvent.click(homeLink);
|
||||
},
|
||||
};
|
||||
|
||||
export const LongNames: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{ name: "Home", link: "/" },
|
||||
{ name: "AI-Powered Writing Assistants", link: "/ai-writing-assistants" },
|
||||
{
|
||||
name: "Advanced Grammar and Style Checker",
|
||||
link: "/ai-writing-assistants/grammar-style-checker",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as React from "react";
|
||||
import Link from "next/link";
|
||||
import { IconLeftArrow, IconRightArrow } from "@/components/ui/icons";
|
||||
|
||||
interface BreadcrumbItem {
|
||||
name: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
interface BreadCrumbsProps {
|
||||
items: BreadcrumbItem[];
|
||||
}
|
||||
|
||||
export const BreadCrumbs: React.FC<BreadCrumbsProps> = ({ items }) => {
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
{/*
|
||||
Commented out for now, but keeping until we have approval to remove
|
||||
<button className="flex h-12 w-12 items-center justify-center rounded-full border border-neutral-200 transition-colors hover:bg-neutral-50 dark:border-neutral-700 dark:hover:bg-neutral-800">
|
||||
<IconLeftArrow className="h-5 w-5 text-neutral-900 dark:text-neutral-100" />
|
||||
</button>
|
||||
<button className="flex h-12 w-12 items-center justify-center rounded-full border border-neutral-200 transition-colors hover:bg-neutral-50 dark:border-neutral-700 dark:hover:bg-neutral-800">
|
||||
<IconRightArrow className="h-5 w-5 text-neutral-900 dark:text-neutral-100" />
|
||||
</button> */}
|
||||
<div className="flex h-auto min-h-[4.375rem] flex-wrap items-center justify-start gap-4 rounded-[5rem] bg-white dark:bg-neutral-900">
|
||||
{items.map((item, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<Link href={item.link}>
|
||||
<span className="rounded py-1 pr-2 font-neue text-xl font-medium leading-9 tracking-tight text-[#272727] transition-colors duration-200 hover:text-gray-400 dark:text-neutral-100 dark:hover:text-gray-500">
|
||||
{item.name}
|
||||
</span>
|
||||
</Link>
|
||||
{index < items.length - 1 && (
|
||||
<span className="font-['SF Pro'] text-center text-2xl font-normal text-black dark:text-neutral-100">
|
||||
/
|
||||
</span>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,220 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Button } from "./Button";
|
||||
import { userEvent, within, expect } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Button",
|
||||
component: Button,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: "select",
|
||||
options: [
|
||||
"default",
|
||||
"destructive",
|
||||
"outline",
|
||||
"secondary",
|
||||
"ghost",
|
||||
"link",
|
||||
],
|
||||
},
|
||||
size: {
|
||||
control: "select",
|
||||
options: ["default", "sm", "lg", "primary", "icon"],
|
||||
},
|
||||
disabled: {
|
||||
control: "boolean",
|
||||
},
|
||||
asChild: {
|
||||
control: "boolean",
|
||||
},
|
||||
children: {
|
||||
control: "text",
|
||||
},
|
||||
onClick: { action: "clicked" },
|
||||
},
|
||||
} satisfies Meta<typeof Button>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: "Button",
|
||||
},
|
||||
};
|
||||
|
||||
export const Interactive: Story = {
|
||||
args: {
|
||||
children: "Interactive Button",
|
||||
},
|
||||
argTypes: {
|
||||
onClick: { action: "clicked" },
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const button = canvas.getByRole("button", { name: /Interactive Button/i });
|
||||
await userEvent.click(button);
|
||||
await expect(button).toHaveFocus();
|
||||
},
|
||||
};
|
||||
|
||||
export const Variants: Story = {
|
||||
render: (args) => (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button {...args} variant="default">
|
||||
Default
|
||||
</Button>
|
||||
<Button {...args} variant="destructive">
|
||||
Destructive
|
||||
</Button>
|
||||
<Button {...args} variant="outline">
|
||||
Outline
|
||||
</Button>
|
||||
<Button {...args} variant="secondary">
|
||||
Secondary
|
||||
</Button>
|
||||
<Button {...args} variant="ghost">
|
||||
Ghost
|
||||
</Button>
|
||||
<Button {...args} variant="link">
|
||||
Link
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const buttons = canvas.getAllByRole("button");
|
||||
await expect(buttons).toHaveLength(6);
|
||||
for (const button of buttons) {
|
||||
await userEvent.hover(button);
|
||||
await expect(button).toHaveAttribute(
|
||||
"class",
|
||||
expect.stringContaining("hover:"),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
export const Sizes: Story = {
|
||||
render: (args) => (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button {...args} size="sm">
|
||||
Small
|
||||
</Button>
|
||||
<Button {...args} size="default">
|
||||
Default
|
||||
</Button>
|
||||
<Button {...args} size="lg">
|
||||
Large
|
||||
</Button>
|
||||
<Button {...args} size="primary">
|
||||
Primary
|
||||
</Button>
|
||||
<Button {...args} size="icon">
|
||||
🚀
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const buttons = canvas.getAllByRole("button");
|
||||
await expect(buttons).toHaveLength(5);
|
||||
const sizeClasses = [
|
||||
"h-8 px-3 py-1.5 text-xs",
|
||||
"h-10 px-4 py-2 text-sm",
|
||||
"h-12 px-5 py-2.5 text-lg",
|
||||
"h-10 w-28",
|
||||
"h-10 w-10",
|
||||
];
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
await expect(buttons[i]).toHaveAttribute(
|
||||
"class",
|
||||
expect.stringContaining(sizeClasses[i]),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
children: "Disabled Button",
|
||||
disabled: true,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const button = canvas.getByRole("button", { name: /Disabled Button/i });
|
||||
await expect(button).toBeDisabled();
|
||||
await expect(button).toHaveAttribute(
|
||||
"class",
|
||||
expect.stringContaining("disabled:opacity-50"),
|
||||
);
|
||||
await expect(button).not.toHaveFocus();
|
||||
},
|
||||
};
|
||||
|
||||
export const WithIcon: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="mr-2 h-4 w-4"
|
||||
>
|
||||
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
|
||||
</svg>
|
||||
Button with Icon
|
||||
</>
|
||||
),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const button = canvas.getByRole("button", { name: /Button with Icon/i });
|
||||
const icon = button.querySelector("svg");
|
||||
await expect(icon).toBeInTheDocument();
|
||||
await expect(button).toHaveTextContent("Button with Icon");
|
||||
},
|
||||
};
|
||||
|
||||
export const LoadingState: Story = {
|
||||
args: {
|
||||
children: "Loading...",
|
||||
disabled: true,
|
||||
},
|
||||
render: (args) => (
|
||||
<Button {...args}>
|
||||
<svg
|
||||
className="mr-2 h-4 w-4 animate-spin"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||
</svg>
|
||||
{args.children}
|
||||
</Button>
|
||||
),
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const button = canvas.getByRole("button", { name: /Loading.../i });
|
||||
await expect(button).toBeDisabled();
|
||||
const spinner = button.querySelector("svg");
|
||||
await expect(spinner).toHaveClass("animate-spin");
|
||||
},
|
||||
};
|
||||
69
autogpt_platform/frontend/src/components/agptui/Button.tsx
Normal file
69
autogpt_platform/frontend/src/components/agptui/Button.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-[80px] text-xl font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 dark:focus-visible:ring-neutral-300 font-neue leading-9 tracking-tight",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-white border border-black/50 text-[#272727] hover:bg-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700",
|
||||
destructive:
|
||||
"bg-red-600 text-neutral-50 border border-red-500/50 hover:bg-red-500/90 dark:bg-red-700 dark:text-neutral-50 dark:hover:bg-red-600",
|
||||
outline:
|
||||
"bg-white border border-black/50 text-[#272727] hover:bg-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700",
|
||||
secondary:
|
||||
"bg-neutral-100 text-[#272727] border border-neutral-200 hover:bg-neutral-100/80 dark:bg-neutral-700 dark:text-neutral-100 dark:border-neutral-600 dark:hover:bg-neutral-600",
|
||||
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",
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"h-10 px-4 py-2 text-sm sm:h-12 sm:px-5 sm:py-2.5 sm:text-base md:h-14 md:px-6 md:py-3 md:text-lg lg:h-[4.375rem] lg:px-[1.625rem] lg:py-[0.4375rem] lg:text-xl",
|
||||
sm: "h-8 px-3 py-1.5 text-xs sm:h-9 sm:px-3.5 sm:py-2 sm:text-sm md:h-10 md:px-4 md:py-2 md:text-base lg:h-[3.125rem] lg:px-[1.25rem] lg:py-[0.3125rem] lg:text-sm",
|
||||
lg: "h-12 px-5 py-2.5 text-lg sm:h-14 sm:px-6 sm:py-3 sm:text-xl md:h-16 md:px-7 md:py-3.5 md:text-2xl lg:h-[5.625rem] lg:px-[2rem] lg:py-[0.5625rem] lg:text-2xl",
|
||||
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]",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
variant?:
|
||||
| "default"
|
||||
| "destructive"
|
||||
| "outline"
|
||||
| "secondary"
|
||||
| "ghost"
|
||||
| "link";
|
||||
size?: "default" | "sm" | "lg" | "primary" | "icon";
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { CreatorCard } from "./CreatorCard";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Creator Card",
|
||||
component: CreatorCard,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
creatorName: { control: "text" },
|
||||
creatorImage: { control: "text" },
|
||||
bio: { control: "text" },
|
||||
agentsUploaded: { control: "number" },
|
||||
onClick: { action: "clicked" },
|
||||
avatarSrc: { control: "text" },
|
||||
},
|
||||
} satisfies Meta<typeof CreatorCard>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
creatorName: "John Doe",
|
||||
creatorImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
bio: "AI enthusiast and developer with a passion for creating innovative agents.",
|
||||
agentsUploaded: 15,
|
||||
onClick: () => console.log("Default CreatorCard clicked"),
|
||||
avatarSrc: "https://github.com/shadcn.png",
|
||||
},
|
||||
};
|
||||
|
||||
export const NewCreator: Story = {
|
||||
args: {
|
||||
creatorName: "Jane Smith",
|
||||
creatorImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
bio: "Excited to start my journey in AI agent development!",
|
||||
agentsUploaded: 1,
|
||||
onClick: () => console.log("NewCreator CreatorCard clicked"),
|
||||
avatarSrc: "https://example.com/avatar2.jpg",
|
||||
},
|
||||
};
|
||||
|
||||
export const ExperiencedCreator: Story = {
|
||||
args: {
|
||||
creatorName: "Alex Johnson",
|
||||
creatorImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
bio: "Veteran AI researcher with a focus on natural language processing and machine learning.",
|
||||
agentsUploaded: 50,
|
||||
onClick: () => console.log("ExperiencedCreator CreatorCard clicked"),
|
||||
avatarSrc: "https://example.com/avatar3.jpg",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInteraction: Story = {
|
||||
args: {
|
||||
creatorName: "Sam Brown",
|
||||
creatorImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
bio: "Exploring the frontiers of AI and its applications in everyday life.",
|
||||
agentsUploaded: 30,
|
||||
onClick: () => console.log("WithInteraction CreatorCard clicked"),
|
||||
avatarSrc: "https://example.com/avatar4.jpg",
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const creatorCard = canvas.getByText("Sam Brown");
|
||||
|
||||
await userEvent.hover(creatorCard);
|
||||
await userEvent.click(creatorCard);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import * as React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
const BACKGROUND_COLORS = [
|
||||
"bg-amber-100 dark:bg-amber-800", // #fef3c7 / #92400e
|
||||
"bg-violet-100 dark:bg-violet-800", // #ede9fe / #5b21b6
|
||||
"bg-green-100 dark:bg-green-800", // #dcfce7 / #065f46
|
||||
"bg-blue-100 dark:bg-blue-800", // #dbeafe / #1e3a8a
|
||||
];
|
||||
|
||||
interface CreatorCardProps {
|
||||
creatorName: string;
|
||||
creatorImage: string;
|
||||
bio: string;
|
||||
agentsUploaded: number;
|
||||
onClick: () => void;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export const CreatorCard: React.FC<CreatorCardProps> = ({
|
||||
creatorName,
|
||||
creatorImage,
|
||||
bio,
|
||||
agentsUploaded,
|
||||
onClick,
|
||||
index,
|
||||
}) => {
|
||||
const backgroundColor = BACKGROUND_COLORS[index % BACKGROUND_COLORS.length];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`h-[264px] w-full px-[18px] pb-5 pt-6 ${backgroundColor} inline-flex cursor-pointer flex-col items-start justify-start gap-3.5 rounded-[26px] transition-all duration-200 hover:brightness-95`}
|
||||
onClick={onClick}
|
||||
data-testid="creator-card"
|
||||
>
|
||||
<div className="relative h-[64px] w-[64px]">
|
||||
<div className="absolute inset-0 overflow-hidden rounded-full">
|
||||
{creatorImage ? (
|
||||
<Image
|
||||
src={creatorImage}
|
||||
alt={creatorName}
|
||||
width={64}
|
||||
height={64}
|
||||
className="h-full w-full object-cover"
|
||||
priority
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full w-full bg-neutral-300 dark:bg-neutral-600" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="font-poppins text-2xl font-semibold leading-tight text-neutral-900 dark:text-neutral-100">
|
||||
{creatorName}
|
||||
</h3>
|
||||
<p className="font-geist text-sm font-normal leading-normal text-neutral-600 dark:text-neutral-400">
|
||||
{bio}
|
||||
</p>
|
||||
<div className="font-geist text-lg font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
|
||||
{agentsUploaded} agents
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { CreatorInfoCard } from "./CreatorInfoCard";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Creator Info Card",
|
||||
component: CreatorInfoCard,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
username: { control: "text" },
|
||||
handle: { control: "text" },
|
||||
avatarSrc: { control: "text" },
|
||||
categories: { control: "object" },
|
||||
averageRating: { control: "number", min: 0, max: 5, step: 0.1 },
|
||||
totalRuns: { control: "number" },
|
||||
},
|
||||
} satisfies Meta<typeof CreatorInfoCard>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
username: "SignificantGravitas",
|
||||
handle: "oliviagrace1421",
|
||||
avatarSrc: "https://github.com/shadcn.png",
|
||||
categories: ["Entertainment", "Business"],
|
||||
averageRating: 4.7,
|
||||
totalRuns: 1500,
|
||||
},
|
||||
};
|
||||
|
||||
export const NewCreator: Story = {
|
||||
args: {
|
||||
username: "AI Enthusiast",
|
||||
handle: "ai_newbie",
|
||||
avatarSrc: "https://example.com/avatar2.jpg",
|
||||
categories: ["AI", "Technology"],
|
||||
averageRating: 0,
|
||||
totalRuns: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const ExperiencedCreator: Story = {
|
||||
args: {
|
||||
username: "Tech Master",
|
||||
handle: "techmaster",
|
||||
avatarSrc: "https://example.com/avatar3.jpg",
|
||||
categories: ["AI", "Development", "Education"],
|
||||
averageRating: 4.9,
|
||||
totalRuns: 50000,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
import * as React from "react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { StarRatingIcons } from "@/components/ui/icons";
|
||||
|
||||
interface CreatorInfoCardProps {
|
||||
username: string;
|
||||
handle: string;
|
||||
avatarSrc: string;
|
||||
categories: string[];
|
||||
averageRating: number;
|
||||
totalRuns: number;
|
||||
}
|
||||
|
||||
export const CreatorInfoCard: React.FC<CreatorInfoCardProps> = ({
|
||||
username,
|
||||
handle,
|
||||
avatarSrc,
|
||||
categories,
|
||||
averageRating,
|
||||
totalRuns,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="inline-flex h-auto min-h-[500px] w-full max-w-[440px] flex-col items-start justify-between rounded-[26px] bg-violet-100 p-4 dark:bg-violet-900 sm:h-[632px] sm:w-[440px] sm:p-6"
|
||||
role="article"
|
||||
aria-label={`Creator profile for ${username}`}
|
||||
>
|
||||
<div className="flex w-full flex-col items-start justify-start gap-3.5 sm:h-[218px]">
|
||||
<Avatar className="h-[100px] w-[100px] sm:h-[130px] sm:w-[130px]">
|
||||
<AvatarImage src={avatarSrc} alt={`${username}'s avatar`} />
|
||||
<AvatarFallback className="h-[100px] w-[100px] sm:h-[130px] sm:w-[130px]">
|
||||
{username.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex w-full flex-col items-start justify-start gap-1.5">
|
||||
<div className="font-poppins w-full text-2xl font-medium leading-8 text-neutral-900 dark:text-neutral-100 sm:text-[35px] sm:leading-10">
|
||||
{username}
|
||||
</div>
|
||||
<div className="w-full font-neue text-lg font-normal leading-6 text-neutral-800 dark:text-neutral-200 sm:text-xl sm:leading-7">
|
||||
@{handle}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-4 flex w-full flex-col items-start justify-start gap-6 sm:gap-[50px]">
|
||||
<div className="flex w-full flex-col items-start justify-start gap-3">
|
||||
<div className="h-px w-full bg-neutral-700 dark:bg-neutral-300" />
|
||||
<div className="flex flex-col items-start justify-start gap-2.5">
|
||||
<div className="w-full font-neue text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
Top categories
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-wrap items-center gap-2.5"
|
||||
role="list"
|
||||
aria-label="Categories"
|
||||
>
|
||||
{categories.map((category, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-center gap-2.5 rounded-[34px] border border-neutral-600 px-5 py-3 dark:border-neutral-400"
|
||||
role="listitem"
|
||||
>
|
||||
<div className="font-neue text-base font-normal leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
{category}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col items-start justify-start gap-3">
|
||||
<div className="h-px w-full bg-neutral-700 dark:bg-neutral-300" />
|
||||
<div className="flex w-full flex-col items-start justify-between gap-4 sm:flex-row sm:gap-0">
|
||||
<div className="flex w-full flex-col items-start justify-start gap-2.5 sm:w-[164px]">
|
||||
<div className="w-full font-neue text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
Average rating
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<div className="font-neue text-lg font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
|
||||
{averageRating.toFixed(1)}
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-px"
|
||||
role="img"
|
||||
aria-label={`Rating: ${averageRating} out of 5 stars`}
|
||||
>
|
||||
{StarRatingIcons(averageRating)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-start justify-start gap-2.5 sm:w-[164px]">
|
||||
<div className="w-full font-neue text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
Number of runs
|
||||
</div>
|
||||
<div className="font-neue text-lg font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
|
||||
{new Intl.NumberFormat().format(totalRuns)} runs
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { CreatorLinks } from "./CreatorLinks";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Creator Links",
|
||||
component: CreatorLinks,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
links: {
|
||||
control: "object",
|
||||
description: "Object containing various social and web links",
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof CreatorLinks>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
links: {
|
||||
website: "https://example.com",
|
||||
linkedin: "https://linkedin.com/in/johndoe",
|
||||
github: "https://github.com/johndoe",
|
||||
other: ["https://twitter.com/johndoe", "https://medium.com/@johndoe"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WebsiteOnly: Story = {
|
||||
args: {
|
||||
links: {
|
||||
website: "https://example.com",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const SocialLinks: Story = {
|
||||
args: {
|
||||
links: {
|
||||
linkedin: "https://linkedin.com/in/janedoe",
|
||||
github: "https://github.com/janedoe",
|
||||
other: ["https://twitter.com/janedoe"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NoLinks: Story = {
|
||||
args: {
|
||||
links: {},
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleOtherLinks: Story = {
|
||||
args: {
|
||||
links: {
|
||||
website: "https://example.com",
|
||||
linkedin: "https://linkedin.com/in/creator",
|
||||
github: "https://github.com/creator",
|
||||
other: [
|
||||
"https://twitter.com/creator",
|
||||
"https://medium.com/@creator",
|
||||
"https://youtube.com/@creator",
|
||||
"https://tiktok.com/@creator",
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as React from "react";
|
||||
import { getIconForSocial } from "@/components/ui/icons";
|
||||
|
||||
interface CreatorLinksProps {
|
||||
links: string[];
|
||||
}
|
||||
|
||||
export const CreatorLinks: React.FC<CreatorLinksProps> = ({ links }) => {
|
||||
if (!links || links.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderLinkButton = (url: string) => (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex min-w-[200px] flex-1 items-center justify-between rounded-[34px] border border-neutral-600 px-5 py-3 dark:border-neutral-400"
|
||||
>
|
||||
<div className="font-neue text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
{new URL(url).hostname.replace("www.", "")}
|
||||
</div>
|
||||
<div className="relative h-6 w-6">
|
||||
{getIconForSocial(url, {
|
||||
className: "h-6 w-6 text-neutral-800 dark:text-neutral-200",
|
||||
})}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start justify-start gap-4">
|
||||
<div className="font-neue text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
Other links
|
||||
</div>
|
||||
<div className="flex w-full flex-wrap gap-3">
|
||||
{links.map((link, index) => (
|
||||
<React.Fragment key={index}>{renderLinkButton(link)}</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import CreditsCard from "./CreditsCard";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
|
||||
const meta: Meta<typeof CreditsCard> = {
|
||||
title: "AGPT UI/Credits Card",
|
||||
component: CreditsCard,
|
||||
tags: ["autodocs"],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CreditsCard>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
credits: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const SmallNumber: Story = {
|
||||
args: {
|
||||
credits: 10,
|
||||
},
|
||||
};
|
||||
|
||||
export const LargeNumber: Story = {
|
||||
args: {
|
||||
credits: 1000000,
|
||||
},
|
||||
};
|
||||
|
||||
export const InteractionTest: Story = {
|
||||
args: {
|
||||
credits: 100,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const refreshButton = canvas.getByRole("button", {
|
||||
name: /refresh credits/i,
|
||||
});
|
||||
await userEvent.click(refreshButton);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { IconRefresh } from "@/components/ui/icons";
|
||||
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
interface CreditsCardProps {
|
||||
credits: number;
|
||||
}
|
||||
|
||||
const CreditsCard = ({ credits }: CreditsCardProps) => {
|
||||
const [currentCredits, setCurrentCredits] = useState(credits);
|
||||
const api = new AutoGPTServerAPI();
|
||||
|
||||
const onRefresh = async () => {
|
||||
const { credits } = await api.getUserCredit("credits-card");
|
||||
setCurrentCredits(credits);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="inline-flex h-[60px] items-center gap-2.5 rounded-2xl bg-neutral-200 p-4 dark:bg-neutral-800">
|
||||
<div className="flex items-center gap-0.5">
|
||||
<span className="p-ui-semibold text-base leading-7 text-neutral-900 dark:text-neutral-50">
|
||||
{currentCredits.toLocaleString()}
|
||||
</span>
|
||||
<span className="p-ui pl-1 text-base leading-7 text-neutral-900 dark:text-neutral-50">
|
||||
credits
|
||||
</span>
|
||||
</div>
|
||||
<Tooltip key="RefreshCredits" delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
className="h-6 w-6 transition-colors hover:text-neutral-700 dark:hover:text-neutral-300"
|
||||
aria-label="Refresh credits"
|
||||
>
|
||||
<IconRefresh className="h-6 w-6" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Refresh credits</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreditsCard;
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { FeaturedStoreCard } from "./FeaturedStoreCard";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Featured Store Card",
|
||||
component: FeaturedStoreCard,
|
||||
parameters: {
|
||||
layout: {
|
||||
center: true,
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
agentName: { control: "text" },
|
||||
subHeading: { control: "text" },
|
||||
agentImage: { control: "text" },
|
||||
creatorImage: { control: "text" },
|
||||
creatorName: { control: "text" },
|
||||
description: { control: "text" },
|
||||
runs: { control: "number" },
|
||||
rating: { control: "number", min: 0, max: 5, step: 0.1 },
|
||||
onClick: { action: "clicked" },
|
||||
},
|
||||
} satisfies Meta<typeof FeaturedStoreCard>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
agentName: "Personalized Morning Coffee Newsletter example of three lines",
|
||||
subHeading:
|
||||
"Transform ideas into breathtaking images with this AI-powered Image Generator.",
|
||||
description:
|
||||
"Elevate your web content with this powerful AI Webpage Copy Improver. Designed for marketers, SEO specialists, and web developers, this tool analyses and enhances website copy for maximum impact. Using advanced language models, it optimizes text for better clarity, SEO performance, and increased conversion rates.",
|
||||
agentImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
creatorImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
creatorName: "AI Solutions Inc.",
|
||||
runs: 50000,
|
||||
rating: 4.7,
|
||||
onClick: () => console.log("Card clicked"),
|
||||
},
|
||||
};
|
||||
|
||||
export const LowRating: Story = {
|
||||
args: {
|
||||
agentName: "Data Analyzer Lite",
|
||||
subHeading: "Basic data analysis tool",
|
||||
description:
|
||||
"A lightweight data analysis tool for basic data processing needs.",
|
||||
agentImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
creatorImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
creatorName: "DataTech",
|
||||
runs: 10000,
|
||||
rating: 2.8,
|
||||
onClick: () => console.log("Card clicked"),
|
||||
},
|
||||
};
|
||||
|
||||
export const HighRuns: Story = {
|
||||
args: {
|
||||
agentName: "CodeAssist AI",
|
||||
subHeading: "Your AI coding companion",
|
||||
description:
|
||||
"An intelligent coding assistant that helps developers write better code faster.",
|
||||
agentImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
creatorImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
creatorName: "DevTools Co.",
|
||||
runs: 1000000,
|
||||
rating: 4.9,
|
||||
onClick: () => console.log("Card clicked"),
|
||||
},
|
||||
};
|
||||
|
||||
export const NoCreatorImage: Story = {
|
||||
args: {
|
||||
agentName: "MultiTasker",
|
||||
subHeading: "All-in-one productivity suite",
|
||||
description:
|
||||
"A comprehensive productivity suite that combines task management, note-taking, and project planning into one seamless interface.",
|
||||
agentImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
creatorName: "Productivity Plus",
|
||||
runs: 75000,
|
||||
rating: 4.5,
|
||||
onClick: () => console.log("Card clicked"),
|
||||
},
|
||||
};
|
||||
|
||||
export const ShortDescription: Story = {
|
||||
args: {
|
||||
agentName: "QuickTask",
|
||||
subHeading: "Fast task automation",
|
||||
description: "Simple and efficient task automation tool.",
|
||||
agentImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
creatorImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
creatorName: "EfficientWorks",
|
||||
runs: 50000,
|
||||
rating: 4.2,
|
||||
onClick: () => console.log("Card clicked"),
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInteraction: Story = {
|
||||
args: {
|
||||
agentName: "AI Writing Assistant",
|
||||
subHeading: "Enhance your writing",
|
||||
description:
|
||||
"An AI-powered writing assistant that helps improve your writing style and clarity.",
|
||||
agentImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
creatorImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
creatorName: "WordCraft AI",
|
||||
runs: 200000,
|
||||
rating: 4.6,
|
||||
onClick: () => console.log("Card clicked"),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const featuredCard = canvas.getByTestId("featured-store-card");
|
||||
await userEvent.hover(featuredCard);
|
||||
await userEvent.click(featuredCard);
|
||||
},
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user