feat(platform): Agent Store V2 (#8874)
# 🌎 Overview AutoGPT Store Version 2 expands on the Pre-Store by enhancing agent discovery, providing richer content presentation, and introducing new user engagement features. The focus is on creating a visually appealing and interactive marketplace that allows users to explore and evaluate agents through images, videos, and detailed descriptions. ### Vision To create a visually compelling and interactive open-source marketplace for autonomous AI agents, where users can easily discover, evaluate, and interact with agents through media-rich listings, ratings, and version history. ### Objectives 📊 Incorporate visuals (icons, images, videos) into agent listings. ⭐ Introduce a rating system and agent run count. 🔄 Provide version history and update logs from creators. 🔍 Improve user experience with advanced search and filtering features. ### Changes 🏗️ <!-- Concisely describe all of the changes made in this pull request: --> ### Checklist 📋 #### For code changes: - [ ] I have clearly listed my changes in the PR description - [ ] I have made a test plan - [ ] I have tested my changes according to the test plan: <!-- Put your test plan here: --> - [ ] ... <details> <summary>Example test plan</summary> - [ ] Create from scratch and execute an agent with at least 3 blocks - [ ] Import an agent from file upload, and confirm it executes correctly - [ ] Upload agent to marketplace - [ ] Import an agent from marketplace and confirm it executes correctly - [ ] Edit an agent from monitor, and confirm it executes correctly </details> #### For configuration changes: - [ ] `.env.example` is updated or already compatible with my changes - [ ] `docker-compose.yml` is updated or already compatible with my changes - [ ] I have included a list of my configuration changes in the PR description (under **Changes**) <details> <summary>Examples of configuration changes</summary> - Changing ports - Adding new services that need to communicate with each other - Secrets or environment variable changes - New or infrastructure changes such as databases </details> --------- Co-authored-by: Bently <tomnoon9@gmail.com> Co-authored-by: Aarushi <aarushik93@gmail.com>pull/8686/head^2
parent
94a312a279
commit
2de5e3dd83
|
@ -42,7 +42,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
browser: [chromium, firefox, webkit]
|
||||
browser: [chromium, webkit]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -0,0 +1,761 @@
|
|||
import logging
|
||||
import random
|
||||
from datetime import datetime
|
||||
|
||||
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}"
|
||||
)
|
||||
sanitized_query = None
|
||||
# Sanitize and validate search query by escaping special characters
|
||||
if search_query is not None:
|
||||
sanitized_query = search_query.strip()
|
||||
if not sanitized_query or len(sanitized_query) > 100: # Reasonable length limit
|
||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Invalid search query"
|
||||
)
|
||||
|
||||
# Escape special SQL characters
|
||||
sanitized_query = (
|
||||
sanitized_query.replace("\\", "\\\\")
|
||||
.replace("%", "\\%")
|
||||
.replace("_", "\\_")
|
||||
.replace("[", "\\[")
|
||||
.replace("]", "\\]")
|
||||
.replace("'", "\\'")
|
||||
.replace('"', '\\"')
|
||||
.replace(";", "\\;")
|
||||
.replace("--", "\\--")
|
||||
.replace("/*", "\\/*")
|
||||
.replace("*/", "\\*/")
|
||||
)
|
||||
|
||||
where_clause = {}
|
||||
if featured:
|
||||
where_clause["featured"] = featured
|
||||
if creator:
|
||||
where_clause["creator_username"] = creator
|
||||
if category:
|
||||
where_clause["categories"] = {"has": category}
|
||||
|
||||
if sanitized_query:
|
||||
where_clause["OR"] = [
|
||||
{"agent_name": {"contains": sanitized_query, "mode": "insensitive"}},
|
||||
{"description": {"contains": sanitized_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 with sanitized inputs
|
||||
where = {}
|
||||
|
||||
# Add search filter if provided, using parameterized queries
|
||||
if search_query:
|
||||
# Sanitize and validate search query by escaping special characters
|
||||
sanitized_query = search_query.strip()
|
||||
if not sanitized_query or len(sanitized_query) > 100: # Reasonable length limit
|
||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Invalid search query"
|
||||
)
|
||||
|
||||
# Escape special SQL characters
|
||||
sanitized_query = (
|
||||
sanitized_query.replace("\\", "\\\\")
|
||||
.replace("%", "\\%")
|
||||
.replace("_", "\\_")
|
||||
.replace("[", "\\[")
|
||||
.replace("]", "\\]")
|
||||
.replace("'", "\\'")
|
||||
.replace('"', '\\"')
|
||||
.replace(";", "\\;")
|
||||
.replace("--", "\\--")
|
||||
.replace("/*", "\\/*")
|
||||
.replace("*/", "\\*/")
|
||||
)
|
||||
|
||||
where["OR"] = [
|
||||
{"username": {"contains": sanitized_query, "mode": "insensitive"}},
|
||||
{"name": {"contains": sanitized_query, "mode": "insensitive"}},
|
||||
{"description": {"contains": sanitized_query, "mode": "insensitive"}},
|
||||
]
|
||||
|
||||
try:
|
||||
# Validate pagination parameters
|
||||
if not isinstance(page, int) or page < 1:
|
||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Invalid page number"
|
||||
)
|
||||
if not isinstance(page_size, int) or page_size < 1 or page_size > 100:
|
||||
raise backend.server.v2.store.exceptions.DatabaseError("Invalid page size")
|
||||
|
||||
# Get total count for pagination using sanitized where clause
|
||||
total = await prisma.models.Creator.prisma().count(
|
||||
where=prisma.types.CreatorWhereInput(**where)
|
||||
)
|
||||
total_pages = (total + page_size - 1) // page_size
|
||||
|
||||
# Add pagination with validated parameters
|
||||
skip = (page - 1) * page_size
|
||||
take = page_size
|
||||
|
||||
# Add sorting with validated sort parameter
|
||||
order = []
|
||||
valid_sort_fields = {"agent_rating", "agent_runs", "num_agents"}
|
||||
if sorted_by in valid_sort_fields:
|
||||
order.append({sorted_by: "desc"})
|
||||
else:
|
||||
order.append({"username": "asc"})
|
||||
|
||||
# Execute query with sanitized parameters
|
||||
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
|
|
@ -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
|
|
@ -0,0 +1,157 @@
|
|||
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:
|
||||
|
||||
# Get file content for deeper validation
|
||||
try:
|
||||
content = await file.read(1024) # Read first 1KB for validation
|
||||
await file.seek(0) # Reset file pointer
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading file content: {str(e)}")
|
||||
raise backend.server.v2.store.exceptions.FileReadError(
|
||||
"Failed to read file content"
|
||||
) from e
|
||||
|
||||
# Validate file signature/magic bytes
|
||||
if file.content_type in ALLOWED_IMAGE_TYPES:
|
||||
# Check image file signatures
|
||||
if content.startswith(b"\xFF\xD8\xFF"): # JPEG
|
||||
if file.content_type != "image/jpeg":
|
||||
raise backend.server.v2.store.exceptions.InvalidFileTypeError(
|
||||
"File signature does not match content type"
|
||||
)
|
||||
elif content.startswith(b"\x89PNG\r\n\x1a\n"): # PNG
|
||||
if file.content_type != "image/png":
|
||||
raise backend.server.v2.store.exceptions.InvalidFileTypeError(
|
||||
"File signature does not match content type"
|
||||
)
|
||||
elif content.startswith(b"GIF87a") or content.startswith(b"GIF89a"): # GIF
|
||||
if file.content_type != "image/gif":
|
||||
raise backend.server.v2.store.exceptions.InvalidFileTypeError(
|
||||
"File signature does not match content type"
|
||||
)
|
||||
elif content.startswith(b"RIFF") and content[8:12] == b"WEBP": # WebP
|
||||
if file.content_type != "image/webp":
|
||||
raise backend.server.v2.store.exceptions.InvalidFileTypeError(
|
||||
"File signature does not match content type"
|
||||
)
|
||||
else:
|
||||
raise backend.server.v2.store.exceptions.InvalidFileTypeError(
|
||||
"Invalid image file signature"
|
||||
)
|
||||
|
||||
elif file.content_type in ALLOWED_VIDEO_TYPES:
|
||||
# Check video file signatures
|
||||
if content.startswith(b"\x00\x00\x00") and (content[4:8] == b"ftyp"): # MP4
|
||||
if file.content_type != "video/mp4":
|
||||
raise backend.server.v2.store.exceptions.InvalidFileTypeError(
|
||||
"File signature does not match content type"
|
||||
)
|
||||
elif content.startswith(b"\x1a\x45\xdf\xa3"): # WebM
|
||||
if file.content_type != "video/webm":
|
||||
raise backend.server.v2.store.exceptions.InvalidFileTypeError(
|
||||
"File signature does not match content type"
|
||||
)
|
||||
else:
|
||||
raise backend.server.v2.store.exceptions.InvalidFileTypeError(
|
||||
"Invalid video file signature"
|
||||
)
|
||||
|
||||
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
|
|
@ -0,0 +1,190 @@
|
|||
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/laptop.jpeg"
|
||||
|
||||
mocker.patch("google.cloud.storage.Client", return_value=mock_client)
|
||||
|
||||
return mock_client
|
||||
|
||||
|
||||
async def test_upload_media_success(mock_settings, mock_storage_client):
|
||||
# Create test JPEG data with valid signature
|
||||
test_data = b"\xFF\xD8\xFF" + b"test data"
|
||||
|
||||
test_file = fastapi.UploadFile(
|
||||
filename="laptop.jpeg",
|
||||
file=io.BytesIO(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/laptop.jpeg"
|
||||
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="laptop.jpeg",
|
||||
file=io.BytesIO(b"\xFF\xD8\xFF" + b"test data"), # Valid JPEG signature
|
||||
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"\x00\x00\x00\x18ftypmp42"), # Valid MP4 signature
|
||||
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/laptop.jpeg"
|
||||
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"\xFF\xD8\xFF" + b"x" * (
|
||||
50 * 1024 * 1024 + 1
|
||||
) # 50MB + 1 byte with valid JPEG signature
|
||||
test_file = fastapi.UploadFile(
|
||||
filename="laptop.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)
|
||||
|
||||
|
||||
async def test_upload_media_file_read_error(mock_settings, mock_storage_client):
|
||||
test_file = fastapi.UploadFile(
|
||||
filename="laptop.jpeg",
|
||||
file=io.BytesIO(b""), # Empty file that will raise error on read
|
||||
headers=starlette.datastructures.Headers({"content-type": "image/jpeg"}),
|
||||
)
|
||||
test_file.read = unittest.mock.AsyncMock(side_effect=Exception("Read error"))
|
||||
|
||||
with pytest.raises(backend.server.v2.store.exceptions.FileReadError):
|
||||
await backend.server.v2.store.media.upload_media("test-user", test_file)
|
||||
|
||||
|
||||
async def test_upload_media_png_success(mock_settings, mock_storage_client):
|
||||
test_file = fastapi.UploadFile(
|
||||
filename="test.png",
|
||||
file=io.BytesIO(b"\x89PNG\r\n\x1a\n"), # Valid PNG signature
|
||||
headers=starlette.datastructures.Headers({"content-type": "image/png"}),
|
||||
)
|
||||
|
||||
result = await backend.server.v2.store.media.upload_media("test-user", test_file)
|
||||
assert result == "http://test-url/media/laptop.jpeg"
|
||||
|
||||
|
||||
async def test_upload_media_gif_success(mock_settings, mock_storage_client):
|
||||
test_file = fastapi.UploadFile(
|
||||
filename="test.gif",
|
||||
file=io.BytesIO(b"GIF89a"), # Valid GIF signature
|
||||
headers=starlette.datastructures.Headers({"content-type": "image/gif"}),
|
||||
)
|
||||
|
||||
result = await backend.server.v2.store.media.upload_media("test-user", test_file)
|
||||
assert result == "http://test-url/media/laptop.jpeg"
|
||||
|
||||
|
||||
async def test_upload_media_webp_success(mock_settings, mock_storage_client):
|
||||
test_file = fastapi.UploadFile(
|
||||
filename="test.webp",
|
||||
file=io.BytesIO(b"RIFF\x00\x00\x00\x00WEBP"), # Valid WebP signature
|
||||
headers=starlette.datastructures.Headers({"content-type": "image/webp"}),
|
||||
)
|
||||
|
||||
result = await backend.server.v2.store.media.upload_media("test-user", test_file)
|
||||
assert result == "http://test-url/media/laptop.jpeg"
|
||||
|
||||
|
||||
async def test_upload_media_webm_success(mock_settings, mock_storage_client):
|
||||
test_file = fastapi.UploadFile(
|
||||
filename="test.webm",
|
||||
file=io.BytesIO(b"\x1a\x45\xdf\xa3"), # Valid WebM signature
|
||||
headers=starlette.datastructures.Headers({"content-type": "video/webm"}),
|
||||
)
|
||||
|
||||
result = await backend.server.v2.store.media.upload_media("test-user", test_file)
|
||||
assert result == "http://test-url/media/laptop.jpeg"
|
||||
|
||||
|
||||
async def test_upload_media_mismatched_signature(mock_settings, mock_storage_client):
|
||||
test_file = fastapi.UploadFile(
|
||||
filename="test.jpeg",
|
||||
file=io.BytesIO(b"\x89PNG\r\n\x1a\n"), # PNG signature with JPEG content type
|
||||
headers=starlette.datastructures.Headers({"content-type": "image/jpeg"}),
|
||||
)
|
||||
|
||||
with pytest.raises(backend.server.v2.store.exceptions.InvalidFileTypeError):
|
||||
await backend.server.v2.store.media.upload_media("test-user", test_file)
|
||||
|
||||
|
||||
async def test_upload_media_invalid_signature(mock_settings, mock_storage_client):
|
||||
test_file = fastapi.UploadFile(
|
||||
filename="test.jpeg",
|
||||
file=io.BytesIO(b"invalid signature"),
|
||||
headers=starlette.datastructures.Headers({"content-type": "image/jpeg"}),
|
||||
)
|
||||
|
||||
with pytest.raises(backend.server.v2.store.exceptions.InvalidFileTypeError):
|
||||
await backend.server.v2.store.media.upload_media("test-user", test_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
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
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.19"
|
||||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,436 @@
|
|||
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 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 CreditTransaction (formerly UserBlockCredit)
|
||||
print(f"Inserting {NUM_USERS} credit transactions")
|
||||
for user in users:
|
||||
for _ in range(1):
|
||||
block = random.choice(agent_blocks)
|
||||
await db.credittransaction.create(
|
||||
data={
|
||||
"transactionKey": str(faker.uuid4()),
|
||||
"userId": user.id,
|
||||
"blockId": block.id,
|
||||
"amount": random.randint(1, 100),
|
||||
"type": (
|
||||
prisma.enums.CreditTransactionType.TOP_UP
|
||||
if random.random() < 0.5
|
||||
else prisma.enums.CreditTransactionType.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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,8 +30,11 @@ 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,
|
||||
},
|
||||
/* Maximum time one test can run for */
|
||||
timeout: 60000,
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
|
@ -40,31 +43,31 @@ export default defineConfig({
|
|||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
|
||||
{
|
||||
name: "firefox",
|
||||
use: { ...devices["Desktop Firefox"] },
|
||||
},
|
||||
// {
|
||||
// name: "firefox",
|
||||
// use: { ...devices["Desktop Firefox"] },
|
||||
// },
|
||||
|
||||
{
|
||||
name: "webkit",
|
||||
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. */
|
||||
{
|
||||
name: "Microsoft Edge",
|
||||
use: { ...devices["Desktop Edge"], channel: "msedge" },
|
||||
},
|
||||
// /* Test against branded browsers. */
|
||||
// {
|
||||
// name: "Microsoft Edge",
|
||||
// use: { ...devices["Desktop Edge"], channel: "msedge" },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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]" />
|
||||
</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",
|
||||
},
|
||||
};
|
|
@ -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();
|
||||
},
|
||||
};
|
|
@ -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");
|
||||
},
|
||||
};
|
|
@ -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;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue