mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 23:28:07 -05:00
feat(platform): Onboarding flow backend (#9511)
This PR adds backend to make Onboarding UI added in https://github.com/Significant-Gravitas/AutoGPT/pull/9485 functional and adds missing confetti screen at the end of Onboarding. *Make sure to have at least any 2 agents in store when testing locally.* Otherwise the onboarding will finish without showing agents. Visit `/onboarding/reset` to reset onboarding state, otherwise it'll always redirect to `/library` once finished. ### Changes 🏗️ - Onboarding opens automatically on sign up and login (if unfinished) for all users - Update db schema to add `UserOnboarding` and add migration - Add GET and PATCH `/onboarding` endpoints and logic to retrieve and update data Onboarding for a user - Update `POST /library/agents` endpoint (`addMarketplaceAgentToLibrary`), so it includes input and output nodes; these are needed to know input schema for the Onboarding - Use new endpoints during onboarding to fetch and update data - Use agents from the marketplace and their input schema for the onboarding - Add algorithm to choose store agents based on user answers and related endpoint `GET /onboarding/agents` - Redirect outside onboarding if finished and resume on proper page - Add `canvas-confetti` and `@types/canvas-confetti` frontend packages - Add and show congrats confetti screen when user runs and agent and before opening library - Minor design updates and onboarding fixes ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Redirect to `/onboarding/*` on sign up and sign in (if onboarding unfinished) - [x] Onboarding works and can be finished - [x] Agent runs on finish - [x] Onboarding state is saved and logging out or refreshing page restores correct state (user choices) - [x] When onboarding finished, trying to go into `/onboarding` redirects to `/library` --------- Co-authored-by: Nicholas Tindle <nicholas.tindle@agpt.co> Co-authored-by: Reinier van der Leer <pwuts@agpt.co>
This commit is contained in:
committed by
GitHub
parent
e5eadeace4
commit
3cf198eea1
247
autogpt_platform/backend/backend/data/onboarding.py
Normal file
247
autogpt_platform/backend/backend/data/onboarding.py
Normal file
@@ -0,0 +1,247 @@
|
||||
import re
|
||||
from typing import Any, Optional
|
||||
|
||||
import prisma
|
||||
import pydantic
|
||||
from prisma import Json
|
||||
from prisma.models import (
|
||||
AgentGraph,
|
||||
AgentGraphExecution,
|
||||
StoreListingVersion,
|
||||
UserOnboarding,
|
||||
)
|
||||
from prisma.types import UserOnboardingUpdateInput
|
||||
|
||||
from backend.server.v2.library.db import set_is_deleted_for_library_agent
|
||||
from backend.server.v2.store.db import get_store_agent_details
|
||||
from backend.server.v2.store.model import StoreAgentDetails
|
||||
|
||||
# Mapping from user reason id to categories to search for when choosing agent to show
|
||||
REASON_MAPPING: dict[str, list[str]] = {
|
||||
"content_marketing": ["writing", "marketing", "creative"],
|
||||
"business_workflow_automation": ["business", "productivity"],
|
||||
"data_research": ["data", "research"],
|
||||
"ai_innovation": ["development", "research"],
|
||||
"personal_productivity": ["personal", "productivity"],
|
||||
}
|
||||
|
||||
|
||||
class UserOnboardingUpdate(pydantic.BaseModel):
|
||||
step: int
|
||||
usageReason: Optional[str] = None
|
||||
integrations: list[str] = pydantic.Field(default_factory=list)
|
||||
otherIntegrations: Optional[str] = None
|
||||
selectedAgentCreator: Optional[str] = None
|
||||
selectedAgentSlug: Optional[str] = None
|
||||
agentInput: Optional[dict[str, Any]] = None
|
||||
isCompleted: bool = False
|
||||
|
||||
|
||||
async def get_user_onboarding(user_id: str):
|
||||
return await UserOnboarding.prisma().upsert(
|
||||
where={"userId": user_id},
|
||||
data={
|
||||
"create": {"userId": user_id}, # type: ignore
|
||||
"update": {},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def update_user_onboarding(user_id: str, data: UserOnboardingUpdate):
|
||||
# Get the user onboarding data
|
||||
user_onboarding = await get_user_onboarding(user_id)
|
||||
update: UserOnboardingUpdateInput = {
|
||||
"step": data.step,
|
||||
"isCompleted": data.isCompleted,
|
||||
}
|
||||
if data.usageReason:
|
||||
update["usageReason"] = data.usageReason
|
||||
if data.integrations:
|
||||
update["integrations"] = data.integrations
|
||||
if data.otherIntegrations:
|
||||
update["otherIntegrations"] = data.otherIntegrations
|
||||
if data.selectedAgentSlug and data.selectedAgentCreator:
|
||||
update["selectedAgentSlug"] = data.selectedAgentSlug
|
||||
update["selectedAgentCreator"] = data.selectedAgentCreator
|
||||
# Check if slug changes
|
||||
if (
|
||||
user_onboarding.selectedAgentCreator
|
||||
and user_onboarding.selectedAgentSlug
|
||||
and user_onboarding.selectedAgentSlug != data.selectedAgentSlug
|
||||
):
|
||||
store_agent = await get_store_agent_details(
|
||||
user_onboarding.selectedAgentCreator, user_onboarding.selectedAgentSlug
|
||||
)
|
||||
store_listing = await StoreListingVersion.prisma().find_unique_or_raise(
|
||||
where={"id": store_agent.store_listing_version_id}
|
||||
)
|
||||
agent_graph = await AgentGraph.prisma().find_first(
|
||||
where={"id": store_listing.agentId, "version": store_listing.version}
|
||||
)
|
||||
execution_count = await AgentGraphExecution.prisma().count(
|
||||
where={
|
||||
"userId": user_id,
|
||||
"agentGraphId": store_listing.agentId,
|
||||
"agentGraphVersion": store_listing.version,
|
||||
}
|
||||
)
|
||||
# If there was no execution and graph doesn't belong to the user,
|
||||
# mark the agent as deleted
|
||||
if execution_count == 0 and agent_graph and agent_graph.userId != user_id:
|
||||
await set_is_deleted_for_library_agent(
|
||||
user_id, store_listing.agentId, store_listing.agentVersion, True
|
||||
)
|
||||
if data.agentInput:
|
||||
update["agentInput"] = Json(data.agentInput)
|
||||
|
||||
return await UserOnboarding.prisma().upsert(
|
||||
where={"userId": user_id},
|
||||
data={
|
||||
"create": {"userId": user_id, **update}, # type: ignore
|
||||
"update": update,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def clean_and_split(text: str) -> list[str]:
|
||||
"""
|
||||
Removes all special characters from a string, truncates it to 100 characters,
|
||||
and splits it by whitespace and commas.
|
||||
|
||||
Args:
|
||||
text (str): The input string.
|
||||
|
||||
Returns:
|
||||
list[str]: A list of cleaned words.
|
||||
"""
|
||||
# Remove all special characters (keep only alphanumeric and whitespace)
|
||||
cleaned_text = re.sub(r"[^a-zA-Z0-9\s,]", "", text.strip()[:100])
|
||||
|
||||
# Split by whitespace and commas
|
||||
words = re.split(r"[\s,]+", cleaned_text)
|
||||
|
||||
# Remove empty strings from the list
|
||||
words = [word.lower() for word in words if word]
|
||||
|
||||
return words
|
||||
|
||||
|
||||
def calculate_points(
|
||||
agent, categories: list[str], custom: list[str], integrations: list[str]
|
||||
) -> int:
|
||||
"""
|
||||
Calculates the total points for an agent based on the specified criteria.
|
||||
|
||||
Args:
|
||||
agent: The agent object.
|
||||
categories (list[str]): List of categories to match.
|
||||
words (list[str]): List of words to match in the description.
|
||||
|
||||
Returns:
|
||||
int: Total points for the agent.
|
||||
"""
|
||||
points = 0
|
||||
|
||||
# 1. Category Matches
|
||||
matched_categories = sum(
|
||||
1 for category in categories if category in agent.categories
|
||||
)
|
||||
points += matched_categories * 100
|
||||
|
||||
# 2. Description Word Matches
|
||||
description_words = agent.description.split() # Split description into words
|
||||
matched_words = sum(1 for word in custom if word in description_words)
|
||||
points += matched_words * 100
|
||||
|
||||
matched_words = sum(1 for word in integrations if word in description_words)
|
||||
points += matched_words * 50
|
||||
|
||||
# 3. Featured Bonus
|
||||
if agent.featured:
|
||||
points += 50
|
||||
|
||||
# 4. Rating Bonus
|
||||
points += agent.rating * 10
|
||||
|
||||
# 5. Runs Bonus
|
||||
runs_points = min(agent.runs / 1000 * 100, 100) # Cap at 100 points
|
||||
points += runs_points
|
||||
|
||||
return int(points)
|
||||
|
||||
|
||||
async def get_recommended_agents(user_id: str) -> list[StoreAgentDetails]:
|
||||
user_onboarding = await get_user_onboarding(user_id)
|
||||
categories = REASON_MAPPING.get(user_onboarding.usageReason or "", [])
|
||||
|
||||
where_clause: dict[str, Any] = {}
|
||||
|
||||
custom = clean_and_split((user_onboarding.usageReason or "").lower())
|
||||
|
||||
if categories:
|
||||
where_clause["OR"] = [
|
||||
{"categories": {"has": category}} for category in categories
|
||||
]
|
||||
else:
|
||||
where_clause["OR"] = [
|
||||
{"description": {"contains": word, "mode": "insensitive"}}
|
||||
for word in custom
|
||||
]
|
||||
|
||||
where_clause["OR"] += [
|
||||
{"description": {"contains": word, "mode": "insensitive"}}
|
||||
for word in user_onboarding.integrations
|
||||
]
|
||||
|
||||
agents = await prisma.models.StoreAgent.prisma().find_many(
|
||||
where=prisma.types.StoreAgentWhereInput(**where_clause),
|
||||
order=[
|
||||
{"featured": "desc"},
|
||||
{"runs": "desc"},
|
||||
{"rating": "desc"},
|
||||
],
|
||||
)
|
||||
|
||||
if len(agents) < 2:
|
||||
agents += await prisma.models.StoreAgent.prisma().find_many(
|
||||
where={
|
||||
"listing_id": {"not_in": [agent.listing_id for agent in agents]},
|
||||
},
|
||||
order=[
|
||||
{"featured": "desc"},
|
||||
{"runs": "desc"},
|
||||
{"rating": "desc"},
|
||||
],
|
||||
take=2 - len(agents),
|
||||
)
|
||||
|
||||
# Calculate points for the first 30 agents and choose the top 2
|
||||
agent_points = []
|
||||
for agent in agents[:50]:
|
||||
points = calculate_points(
|
||||
agent, categories, custom, user_onboarding.integrations
|
||||
)
|
||||
agent_points.append((agent, points))
|
||||
|
||||
agent_points.sort(key=lambda x: x[1], reverse=True)
|
||||
recommended_agents = [agent for agent, _ in agent_points[:2]]
|
||||
|
||||
return [
|
||||
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,
|
||||
)
|
||||
for agent in recommended_agents
|
||||
]
|
||||
@@ -43,6 +43,12 @@ from backend.data.credit import (
|
||||
set_auto_top_up,
|
||||
)
|
||||
from backend.data.notifications import NotificationPreference, NotificationPreferenceDTO
|
||||
from backend.data.onboarding import (
|
||||
UserOnboardingUpdate,
|
||||
get_recommended_agents,
|
||||
get_user_onboarding,
|
||||
update_user_onboarding,
|
||||
)
|
||||
from backend.data.user import (
|
||||
get_or_create_user,
|
||||
get_user_notification_preference,
|
||||
@@ -152,6 +158,38 @@ async def update_preferences(
|
||||
return output
|
||||
|
||||
|
||||
########################################################
|
||||
##################### Onboarding #######################
|
||||
########################################################
|
||||
|
||||
|
||||
@v1_router.get(
|
||||
"/onboarding", tags=["onboarding"], dependencies=[Depends(auth_middleware)]
|
||||
)
|
||||
async def get_onboarding(user_id: Annotated[str, Depends(get_user_id)]):
|
||||
return await get_user_onboarding(user_id)
|
||||
|
||||
|
||||
@v1_router.patch(
|
||||
"/onboarding", tags=["onboarding"], dependencies=[Depends(auth_middleware)]
|
||||
)
|
||||
async def update_onboarding(
|
||||
user_id: Annotated[str, Depends(get_user_id)], data: UserOnboardingUpdate
|
||||
):
|
||||
return await update_user_onboarding(user_id, data)
|
||||
|
||||
|
||||
@v1_router.get(
|
||||
"/onboarding/agents",
|
||||
tags=["onboarding"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
async def get_onboarding_agents(
|
||||
user_id: Annotated[str, Depends(get_user_id)],
|
||||
):
|
||||
return await get_recommended_agents(user_id)
|
||||
|
||||
|
||||
########################################################
|
||||
##################### Blocks ###########################
|
||||
########################################################
|
||||
|
||||
@@ -14,6 +14,7 @@ import backend.server.v2.library.model as library_model
|
||||
import backend.server.v2.store.exceptions as store_exceptions
|
||||
import backend.server.v2.store.image_gen as store_image_gen
|
||||
import backend.server.v2.store.media as store_media
|
||||
from backend.data.db import locked_transaction
|
||||
from backend.data.includes import library_agent_include
|
||||
from backend.util.settings import Config
|
||||
|
||||
@@ -362,56 +363,70 @@ async def add_store_agent_to_library(
|
||||
)
|
||||
|
||||
try:
|
||||
store_listing_version = (
|
||||
await prisma.models.StoreListingVersion.prisma().find_unique(
|
||||
where={"id": store_listing_version_id}, include={"Agent": True}
|
||||
)
|
||||
)
|
||||
if not store_listing_version or not store_listing_version.Agent:
|
||||
logger.warning(
|
||||
f"Store listing version not found: {store_listing_version_id}"
|
||||
)
|
||||
raise store_exceptions.AgentNotFoundError(
|
||||
f"Store listing version {store_listing_version_id} not found or invalid"
|
||||
async with locked_transaction(f"user_trx_{user_id}"):
|
||||
store_listing_version = (
|
||||
await prisma.models.StoreListingVersion.prisma().find_unique(
|
||||
where={"id": store_listing_version_id}, include={"Agent": True}
|
||||
)
|
||||
)
|
||||
if not store_listing_version or not store_listing_version.Agent:
|
||||
logger.warning(
|
||||
f"Store listing version not found: {store_listing_version_id}"
|
||||
)
|
||||
raise store_exceptions.AgentNotFoundError(
|
||||
f"Store listing version {store_listing_version_id} not found or invalid"
|
||||
)
|
||||
|
||||
graph = store_listing_version.Agent
|
||||
if graph.userId == user_id:
|
||||
logger.warning(
|
||||
f"User #{user_id} attempted to add their own agent to their library"
|
||||
)
|
||||
raise store_exceptions.DatabaseError("Cannot add own agent to library")
|
||||
graph = store_listing_version.Agent
|
||||
if graph.userId == user_id:
|
||||
logger.warning(
|
||||
f"User #{user_id} attempted to add their own agent to their library"
|
||||
)
|
||||
raise store_exceptions.DatabaseError("Cannot add own agent to library")
|
||||
|
||||
# Check if user already has this agent
|
||||
existing_library_agent = await prisma.models.LibraryAgent.prisma().find_first(
|
||||
where={
|
||||
"userId": user_id,
|
||||
"agentId": graph.id,
|
||||
"agentVersion": graph.version,
|
||||
},
|
||||
include=library_agent_include(user_id),
|
||||
)
|
||||
if existing_library_agent:
|
||||
logger.debug(
|
||||
f"User #{user_id} already has graph #{graph.id} in their library"
|
||||
)
|
||||
return library_model.LibraryAgent.from_db(existing_library_agent)
|
||||
|
||||
# Create LibraryAgent entry
|
||||
added_agent = await prisma.models.LibraryAgent.prisma().create(
|
||||
data={
|
||||
"userId": user_id,
|
||||
"Agent": {
|
||||
"connect": {
|
||||
"graphVersionId": {"id": graph.id, "version": graph.version}
|
||||
# Check if user already has this agent
|
||||
existing_library_agent = (
|
||||
await prisma.models.LibraryAgent.prisma().find_first(
|
||||
where={
|
||||
"userId": user_id,
|
||||
"agentId": graph.id,
|
||||
"agentVersion": graph.version,
|
||||
},
|
||||
include=library_agent_include(user_id),
|
||||
)
|
||||
)
|
||||
if existing_library_agent:
|
||||
if existing_library_agent.isDeleted:
|
||||
# Even if agent exists it needs to be marked as not deleted
|
||||
await set_is_deleted_for_library_agent(
|
||||
user_id, graph.id, graph.version, False
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
f"User #{user_id} already has graph #{graph.id} "
|
||||
"in their library"
|
||||
)
|
||||
return library_model.LibraryAgent.from_db(existing_library_agent)
|
||||
|
||||
# Create LibraryAgent entry
|
||||
added_agent = await prisma.models.LibraryAgent.prisma().create(
|
||||
data={
|
||||
"userId": user_id,
|
||||
"Agent": {
|
||||
"connect": {
|
||||
"graphVersionId": {"id": graph.id, "version": graph.version}
|
||||
},
|
||||
},
|
||||
"isCreatedByUser": False,
|
||||
},
|
||||
"isCreatedByUser": False,
|
||||
},
|
||||
include=library_agent_include(user_id),
|
||||
)
|
||||
logger.debug(f"Added agent #{graph.id} to library for user #{user_id}")
|
||||
return library_model.LibraryAgent.from_db(added_agent)
|
||||
include=library_agent_include(user_id),
|
||||
)
|
||||
logger.debug(
|
||||
f"Added graph #{graph.id} "
|
||||
f"for store listing #{store_listing_version.id} "
|
||||
f"to library for user #{user_id}"
|
||||
)
|
||||
return library_model.LibraryAgent.from_db(added_agent)
|
||||
|
||||
except store_exceptions.AgentNotFoundError:
|
||||
# Reraise for external handling.
|
||||
@@ -421,6 +436,45 @@ async def add_store_agent_to_library(
|
||||
raise store_exceptions.DatabaseError("Failed to add agent to library") from e
|
||||
|
||||
|
||||
async def set_is_deleted_for_library_agent(
|
||||
user_id: str, agent_id: str, agent_version: int, is_deleted: bool
|
||||
) -> None:
|
||||
"""
|
||||
Changes the isDeleted flag for a library agent.
|
||||
|
||||
Args:
|
||||
user_id: The user's library from which the agent is being removed.
|
||||
agent_id: The ID of the agent to remove.
|
||||
agent_version: The version of the agent to remove.
|
||||
is_deleted: Whether the agent is being marked as deleted.
|
||||
|
||||
Raises:
|
||||
DatabaseError: If there's an issue updating the Library
|
||||
"""
|
||||
logger.debug(
|
||||
f"Setting isDeleted={is_deleted} for agent {agent_id} v{agent_version} "
|
||||
f"in library for user {user_id}"
|
||||
)
|
||||
try:
|
||||
logger.warning(
|
||||
f"Setting isDeleted={is_deleted} for agent {agent_id} v{agent_version} in library for user {user_id}"
|
||||
)
|
||||
count = await prisma.models.LibraryAgent.prisma().update_many(
|
||||
where={
|
||||
"userId": user_id,
|
||||
"agentId": agent_id,
|
||||
"agentVersion": agent_version,
|
||||
},
|
||||
data={"isDeleted": is_deleted},
|
||||
)
|
||||
logger.warning(f"Updated {count} isDeleted library agents")
|
||||
except prisma.errors.PrismaError as e:
|
||||
logger.error(f"Database error setting agent isDeleted: {e}")
|
||||
raise store_exceptions.DatabaseError(
|
||||
"Failed to set agent isDeleted in library"
|
||||
) from e
|
||||
|
||||
|
||||
##############################################
|
||||
########### Presets DB Functions #############
|
||||
##############################################
|
||||
|
||||
@@ -106,7 +106,7 @@ async def add_marketplace_agent_to_library(
|
||||
user_id: ID of the authenticated user.
|
||||
|
||||
Returns:
|
||||
201 (Created) on success.
|
||||
library_model.LibraryAgent: Agent added to the library
|
||||
|
||||
Raises:
|
||||
HTTPException(404): If the listing version is not found.
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
-- Create UserOnboarding table
|
||||
CREATE TABLE "UserOnboarding" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"step" INTEGER NOT NULL DEFAULT 0,
|
||||
"usageReason" TEXT,
|
||||
"integrations" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
"otherIntegrations" TEXT,
|
||||
"selectedAgentCreator" TEXT,
|
||||
"selectedAgentSlug" TEXT,
|
||||
"agentInput" JSONB,
|
||||
"isCompleted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"userId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "UserOnboarding_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- Create unique constraint on userId
|
||||
ALTER TABLE "UserOnboarding" ADD CONSTRAINT "UserOnboarding_userId_key" UNIQUE ("userId");
|
||||
|
||||
-- Create index on userId
|
||||
CREATE INDEX "UserOnboarding_userId_idx" ON "UserOnboarding"("userId");
|
||||
|
||||
-- Add foreign key constraint
|
||||
ALTER TABLE "UserOnboarding" ADD CONSTRAINT "UserOnboarding_userId_fkey"
|
||||
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -46,6 +46,7 @@ model User {
|
||||
LibraryAgent LibraryAgent[]
|
||||
|
||||
Profile Profile[]
|
||||
UserOnboarding UserOnboarding?
|
||||
StoreListing StoreListing[]
|
||||
StoreListingReview StoreListingReview[]
|
||||
StoreListingSubmission StoreListingSubmission[]
|
||||
@@ -57,6 +58,25 @@ model User {
|
||||
@@index([email])
|
||||
}
|
||||
|
||||
model UserOnboarding {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
step Int @default(0)
|
||||
usageReason String?
|
||||
integrations String[] @default([])
|
||||
otherIntegrations String?
|
||||
selectedAgentCreator String?
|
||||
selectedAgentSlug String?
|
||||
agentInput Json?
|
||||
isCompleted Boolean @default(false)
|
||||
|
||||
userId String @unique
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
// This model describes the Agent Graph/Flow (Multi Agent System).
|
||||
model AgentGraph {
|
||||
id String @default(uuid())
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"@xyflow/react": "^12.4.2",
|
||||
"ajv": "^8.17.1",
|
||||
"boring-avatars": "^1.11.2",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
@@ -94,7 +95,8 @@
|
||||
"@storybook/nextjs": "^8.5.3",
|
||||
"@storybook/react": "^8.3.5",
|
||||
"@storybook/test": "^8.3.5",
|
||||
"@storybook/test-runner": "^0.20.1",
|
||||
"@storybook/test-runner": "^0.21.0",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/lodash": "^4.17.13",
|
||||
"@types/negotiator": "^0.6.3",
|
||||
"@types/node": "^22.13.0",
|
||||
|
||||
@@ -49,11 +49,14 @@ export async function login(values: z.infer<typeof loginFormSchema>) {
|
||||
}
|
||||
|
||||
await api.createUser();
|
||||
if (!(await api.getUserOnboarding()).isCompleted) {
|
||||
revalidatePath("/onboarding", "layout");
|
||||
redirect("/onboarding");
|
||||
}
|
||||
|
||||
if (data.session) {
|
||||
await supabase.auth.setSession(data.session);
|
||||
}
|
||||
console.log("Logged in");
|
||||
revalidatePath("/", "layout");
|
||||
redirect("/");
|
||||
});
|
||||
@@ -86,7 +89,10 @@ export async function providerLogin(provider: LoginProvider) {
|
||||
}
|
||||
|
||||
await api.createUser();
|
||||
console.log("Logged in");
|
||||
if (!(await api.getUserOnboarding()).isCompleted) {
|
||||
revalidatePath("/onboarding", "layout");
|
||||
redirect("/onboarding");
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import OnboardingButton from "@/components/onboarding/OnboardingButton";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function Page() {
|
||||
const {} = useOnboarding(1);
|
||||
// Just set step to 1
|
||||
useOnboarding(1);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -56,13 +56,13 @@ export default function Page() {
|
||||
</OnboardingHeader>
|
||||
<OnboardingList
|
||||
elements={reasons}
|
||||
selectedId={state.usageReason}
|
||||
selectedId={state?.usageReason}
|
||||
onSelect={(usageReason) => setState({ usageReason })}
|
||||
/>
|
||||
<OnboardingFooter>
|
||||
<OnboardingButton
|
||||
href="/onboarding/3-services"
|
||||
disabled={isEmptyOrWhitespace(state.usageReason)}
|
||||
disabled={isEmptyOrWhitespace(state?.usageReason)}
|
||||
>
|
||||
Next
|
||||
</OnboardingButton>
|
||||
|
||||
@@ -118,13 +118,17 @@ export default function Page() {
|
||||
|
||||
const switchIntegration = useCallback(
|
||||
(name: string) => {
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
const integrations = state.integrations.includes(name)
|
||||
? state.integrations.filter((i) => i !== name)
|
||||
: [...state.integrations, name];
|
||||
|
||||
setState({ integrations });
|
||||
},
|
||||
[state.integrations, setState],
|
||||
[state, setState],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -144,7 +148,7 @@ export default function Page() {
|
||||
</OnboardingText>
|
||||
<OnboardingGrid
|
||||
elements={services}
|
||||
selected={state.integrations}
|
||||
selected={state?.integrations}
|
||||
onSelect={switchIntegration}
|
||||
/>
|
||||
<OnboardingText className="mt-12" variant="subheader">
|
||||
@@ -156,7 +160,7 @@ export default function Page() {
|
||||
<OnboardingInput
|
||||
className="mb-4"
|
||||
placeholder="Others (please specify)"
|
||||
value={state.otherIntegrations || ""}
|
||||
value={state?.otherIntegrations || ""}
|
||||
onChange={(otherIntegrations) => setState({ otherIntegrations })}
|
||||
/>
|
||||
</div>
|
||||
@@ -166,7 +170,7 @@ export default function Page() {
|
||||
className="mb-2"
|
||||
href="/onboarding/4-agent"
|
||||
disabled={
|
||||
state.integrations.length === 0 &&
|
||||
state?.integrations.length === 0 &&
|
||||
isEmptyOrWhitespace(state.otherIntegrations)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -8,29 +8,10 @@ import {
|
||||
} from "@/components/onboarding/OnboardingStep";
|
||||
import { OnboardingText } from "@/components/onboarding/OnboardingText";
|
||||
import OnboardingAgentCard from "@/components/onboarding/OnboardingAgentCard";
|
||||
|
||||
const agents = [
|
||||
{
|
||||
id: "0",
|
||||
image: "/placeholder.png",
|
||||
name: "Viral News Video Creator: AI TikTok Shorts",
|
||||
description:
|
||||
"Description of what the agent does. Written by the creator. Example of text that's longer than two lines. Lorem ipsum set dolor amet bacon ipsum dolor amet kielbasa chicken ullamco frankfurter cupim nisi. Esse jerky turkey pancetta lorem officia ad qui ut ham hock venison ut pig mollit ball tip. Tempor chicken eiusmod tongue tail pork belly labore kielbasa consequat culpa cow aliqua. Ea tail dolore sausage flank.",
|
||||
author: "Pwuts",
|
||||
runs: 1539,
|
||||
rating: 4.1,
|
||||
},
|
||||
{
|
||||
id: "1",
|
||||
image: "/placeholder.png",
|
||||
name: "Financial Analysis Agent: Your Personalized Financial Insights Tool",
|
||||
description:
|
||||
"Description of what the agent does. Written by the creator. Example of text that's longer than two lines. Lorem ipsum set dolor amet bacon ipsum dolor amet kielbasa chicken ullamco frankfurter cupim nisi. Esse jerky turkey pancetta lorem officia ad qui ut ham hock venison ut pig mollit ball tip. Tempor chicken eiusmod tongue tail pork belly labore kielbasa consequat culpa cow aliqua. Ea tail dolore sausage flank.",
|
||||
author: "John Ababseh",
|
||||
runs: 824,
|
||||
rating: 4.5,
|
||||
},
|
||||
];
|
||||
import { useEffect, useState } from "react";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { StoreAgentDetails } from "@/lib/autogpt-server-api";
|
||||
import { finishOnboarding } from "../6-congrats/actions";
|
||||
|
||||
function isEmptyOrWhitespace(str: string | undefined | null): boolean {
|
||||
return !str || str.trim().length === 0;
|
||||
@@ -38,6 +19,31 @@ function isEmptyOrWhitespace(str: string | undefined | null): boolean {
|
||||
|
||||
export default function Page() {
|
||||
const { state, setState } = useOnboarding(4);
|
||||
const [agents, setAgents] = useState<StoreAgentDetails[]>([]);
|
||||
const api = useBackendAPI();
|
||||
|
||||
useEffect(() => {
|
||||
api.getOnboardingAgents().then((agents) => {
|
||||
if (agents.length < 2) {
|
||||
finishOnboarding();
|
||||
}
|
||||
setAgents(agents);
|
||||
});
|
||||
}, [api, setAgents]);
|
||||
|
||||
useEffect(() => {
|
||||
// Deselect agent if it's not in the list of agents
|
||||
if (
|
||||
state?.selectedAgentSlug &&
|
||||
!agents.some((agent) => agent.slug === state.selectedAgentSlug)
|
||||
) {
|
||||
setState({
|
||||
selectedAgentSlug: undefined,
|
||||
selectedAgentCreator: undefined,
|
||||
agentInput: undefined,
|
||||
});
|
||||
}
|
||||
}, [state?.selectedAgentSlug, setState, agents]);
|
||||
|
||||
return (
|
||||
<OnboardingStep>
|
||||
@@ -52,21 +58,39 @@ export default function Page() {
|
||||
|
||||
<div className="my-12 flex items-center justify-between gap-5">
|
||||
<OnboardingAgentCard
|
||||
{...agents[0]}
|
||||
selected={state.chosenAgentId == "0"}
|
||||
onClick={() => setState({ chosenAgentId: "0" })}
|
||||
{...(agents[0] || {})}
|
||||
selected={
|
||||
agents[0] !== undefined
|
||||
? state?.selectedAgentSlug == agents[0]?.slug
|
||||
: false
|
||||
}
|
||||
onClick={() =>
|
||||
setState({
|
||||
selectedAgentSlug: agents[0].slug,
|
||||
selectedAgentCreator: agents[0].creator,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<OnboardingAgentCard
|
||||
{...agents[1]}
|
||||
selected={state.chosenAgentId == "1"}
|
||||
onClick={() => setState({ chosenAgentId: "1" })}
|
||||
{...(agents[1] || {})}
|
||||
selected={
|
||||
agents[1] !== undefined
|
||||
? state?.selectedAgentSlug == agents[1]?.slug
|
||||
: false
|
||||
}
|
||||
onClick={() =>
|
||||
setState({
|
||||
selectedAgentSlug: agents[1].slug,
|
||||
selectedAgentCreator: agents[1].creator,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<OnboardingFooter>
|
||||
<OnboardingButton
|
||||
href="/onboarding/5-run"
|
||||
disabled={isEmptyOrWhitespace(state.chosenAgentId)}
|
||||
disabled={isEmptyOrWhitespace(state?.selectedAgentSlug)}
|
||||
>
|
||||
Next
|
||||
</OnboardingButton>
|
||||
|
||||
@@ -9,57 +9,80 @@ import { useOnboarding } from "../layout";
|
||||
import StarRating from "@/components/onboarding/StarRating";
|
||||
import { Play } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import OnboardingAgentInput from "@/components/onboarding/OnboardingAgentInput";
|
||||
import Image from "next/image";
|
||||
|
||||
const agents = [
|
||||
{
|
||||
id: "0",
|
||||
image: "/placeholder.png",
|
||||
name: "Viral News Video Creator: AI TikTok Shorts",
|
||||
description:
|
||||
"Description of what the agent does. Written by the creator. Example of text that's longer than two lines. Lorem ipsum set dolor amet bacon ipsum dolor amet kielbasa chicken ullamco frankfurter cupim nisi. Esse jerky turkey pancetta lorem officia ad qui ut ham hock venison ut pig mollit ball tip. Tempor chicken eiusmod tongue tail pork belly labore kielbasa consequat culpa cow aliqua. Ea tail dolore sausage flank.",
|
||||
author: "Pwuts",
|
||||
runs: 1539,
|
||||
rating: 4.1,
|
||||
},
|
||||
{
|
||||
id: "1",
|
||||
image: "/placeholder.png",
|
||||
name: "Financial Analysis Agent: Your Personalized Financial Insights Tool",
|
||||
description:
|
||||
"Description of what the agent does. Written by the creator. Example of text that's longer than two lines. Lorem ipsum set dolor amet bacon ipsum dolor amet kielbasa chicken ullamco frankfurter cupim nisi. Esse jerky turkey pancetta lorem officia ad qui ut ham hock venison ut pig mollit ball tip. Tempor chicken eiusmod tongue tail pork belly labore kielbasa consequat culpa cow aliqua. Ea tail dolore sausage flank.",
|
||||
author: "John Ababseh",
|
||||
runs: 824,
|
||||
rating: 4.5,
|
||||
},
|
||||
];
|
||||
|
||||
function isEmptyOrWhitespace(str: string | undefined | null): boolean {
|
||||
return !str || str.trim().length === 0;
|
||||
}
|
||||
import { LibraryAgent, StoreAgentDetails } from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
const { state, setState } = useOnboarding(5);
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const selectedAgent = agents.find(
|
||||
(agent) => agent.id === state.chosenAgentId,
|
||||
);
|
||||
const [storeAgent, setStoreAgent] = useState<StoreAgentDetails | null>(null);
|
||||
const [agent, setAgent] = useState<LibraryAgent | null>(null);
|
||||
const router = useRouter();
|
||||
const api = useBackendAPI();
|
||||
|
||||
useEffect(() => {
|
||||
if (!state?.selectedAgentCreator || !state?.selectedAgentSlug) {
|
||||
return;
|
||||
}
|
||||
api
|
||||
.getStoreAgent(state?.selectedAgentCreator!, state?.selectedAgentSlug!)
|
||||
.then((agent) => {
|
||||
setStoreAgent(agent);
|
||||
api
|
||||
.addMarketplaceAgentToLibrary(agent?.store_listing_version_id!)
|
||||
.then((agent) => {
|
||||
setAgent(agent);
|
||||
const update: { [key: string]: any } = {};
|
||||
// Set default values from schema
|
||||
Object.entries(agent?.input_schema?.properties || {}).forEach(
|
||||
([key, value]) => {
|
||||
// Skip if already set
|
||||
if (state?.agentInput && state?.agentInput[key]) {
|
||||
update[key] = state?.agentInput[key];
|
||||
return;
|
||||
}
|
||||
update[key] = value.type !== "null" ? value.default || "" : "";
|
||||
},
|
||||
);
|
||||
setState({
|
||||
agentInput: update,
|
||||
});
|
||||
});
|
||||
});
|
||||
}, [
|
||||
api,
|
||||
setAgent,
|
||||
setStoreAgent,
|
||||
setState,
|
||||
state?.selectedAgentCreator,
|
||||
state?.selectedAgentSlug,
|
||||
]);
|
||||
|
||||
const setAgentInput = useCallback(
|
||||
(key: string, value: string) => {
|
||||
setState({
|
||||
...state,
|
||||
agentInput: {
|
||||
...state.agentInput,
|
||||
...state?.agentInput,
|
||||
[key]: value,
|
||||
},
|
||||
});
|
||||
},
|
||||
[state, setState],
|
||||
[state, state?.agentInput, setState],
|
||||
);
|
||||
|
||||
const runAgent = useCallback(() => {
|
||||
if (!agent) {
|
||||
return;
|
||||
}
|
||||
api.executeGraph(agent.agent_id, agent.agent_version, state?.agentInput);
|
||||
router.push("/onboarding/6-congrats");
|
||||
}, [api, agent, router]);
|
||||
|
||||
const runYourAgent = (
|
||||
<div className="ml-[54px] w-[481px] pl-5">
|
||||
<div className="flex flex-col">
|
||||
@@ -115,11 +138,10 @@ export default function Page() {
|
||||
<span className="font-sans text-xs font-medium tracking-wide text-zinc-500">
|
||||
SELECTED AGENT
|
||||
</span>
|
||||
|
||||
<div className="mt-4 flex h-20 rounded-lg bg-violet-50 p-2">
|
||||
{/* Left image */}
|
||||
<Image
|
||||
src="/placeholder.png"
|
||||
src={storeAgent?.agent_image[0] || ""}
|
||||
alt="Description"
|
||||
width={350}
|
||||
height={196}
|
||||
@@ -129,26 +151,25 @@ export default function Page() {
|
||||
{/* Right content */}
|
||||
<div className="ml-2 flex flex-1 flex-col">
|
||||
<span className="w-[292px] truncate font-sans text-[14px] font-medium leading-normal text-zinc-800">
|
||||
{selectedAgent?.name}
|
||||
{agent?.name}
|
||||
</span>
|
||||
<span className="mt-[5px] w-[292px] truncate font-sans text-xs font-normal leading-tight text-zinc-600">
|
||||
by {selectedAgent?.author}
|
||||
by {storeAgent?.creator}
|
||||
</span>
|
||||
<div className="mt-auto flex w-[292px] justify-between">
|
||||
<span className="mt-1 truncate font-sans text-xs font-normal leading-tight text-zinc-600">
|
||||
{selectedAgent?.runs.toLocaleString("en-US")} runs
|
||||
{storeAgent?.runs.toLocaleString("en-US")} runs
|
||||
</span>
|
||||
<StarRating
|
||||
className="font-sans text-xs font-normal leading-tight text-zinc-600"
|
||||
starSize={12}
|
||||
rating={selectedAgent?.rating || 0}
|
||||
rating={storeAgent?.rating || 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side */}
|
||||
{!showInput ? (
|
||||
runYourAgent
|
||||
@@ -169,28 +190,28 @@ export default function Page() {
|
||||
<OnboardingText className="mb-3 font-semibold" variant="header">
|
||||
Input
|
||||
</OnboardingText>
|
||||
<OnboardingAgentInput
|
||||
name={"Video Count"}
|
||||
description={"The number of videos you'd like to generate"}
|
||||
placeholder={"eg. 1"}
|
||||
value={state.agentInput?.videoCount || ""}
|
||||
onChange={(v) => setAgentInput("videoCount", v)}
|
||||
/>
|
||||
<OnboardingAgentInput
|
||||
name={"Source Website"}
|
||||
description={"The website to source the stories from"}
|
||||
placeholder={"eg. youtube URL"}
|
||||
value={state.agentInput?.sourceWebsite || ""}
|
||||
onChange={(v) => setAgentInput("sourceWebsite", v)}
|
||||
/>
|
||||
{Object.entries(agent?.input_schema?.properties || {}).map(
|
||||
([key, value]) => (
|
||||
<OnboardingAgentInput
|
||||
key={key}
|
||||
name={value.title!}
|
||||
description={value.description || ""}
|
||||
placeholder={value.placeholder || ""}
|
||||
value={state?.agentInput?.[key] || ""}
|
||||
onChange={(v) => setAgentInput(key, v)}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<OnboardingButton
|
||||
variant="violet"
|
||||
className="mt-8 w-[136px]"
|
||||
disabled={
|
||||
isEmptyOrWhitespace(state.agentInput?.videoCount) ||
|
||||
isEmptyOrWhitespace(state.agentInput?.sourceWebsite)
|
||||
Object.values(state?.agentInput || {}).some(
|
||||
(value) => value.trim() === "",
|
||||
) || !agent
|
||||
}
|
||||
onClick={runAgent}
|
||||
>
|
||||
<Play className="" size={18} />
|
||||
Run agent
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"use server";
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export async function finishOnboarding() {
|
||||
const api = new BackendAPI();
|
||||
await api.updateUserOnboarding({ step: 5, isCompleted: true });
|
||||
revalidatePath("/library", "layout");
|
||||
redirect("/library");
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { finishOnboarding } from "./actions";
|
||||
import confetti from "canvas-confetti";
|
||||
|
||||
export default function Page() {
|
||||
const [showText, setShowText] = useState(false);
|
||||
const [showSubtext, setShowSubtext] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
confetti({
|
||||
particleCount: 100,
|
||||
spread: 360,
|
||||
shapes: ["square", "circle"],
|
||||
scalar: 2,
|
||||
decay: 0.92,
|
||||
origin: { y: 0.4, x: 0.5 },
|
||||
});
|
||||
|
||||
const timer0 = setTimeout(() => {
|
||||
setShowText(true);
|
||||
}, 100);
|
||||
|
||||
const timer1 = setTimeout(() => {
|
||||
setShowSubtext(true);
|
||||
}, 500);
|
||||
|
||||
const timer2 = setTimeout(() => {
|
||||
finishOnboarding();
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer0);
|
||||
clearTimeout(timer1);
|
||||
clearTimeout(timer2);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center bg-violet-100">
|
||||
<div
|
||||
className={cn(
|
||||
"z-10 -mb-16 text-9xl duration-500",
|
||||
showText ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
>
|
||||
🎉
|
||||
</div>
|
||||
<h1
|
||||
className={cn(
|
||||
"font-poppins text-9xl font-medium tracking-tighter text-violet-700 duration-500",
|
||||
showText ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
>
|
||||
Congrats!
|
||||
</h1>
|
||||
<p
|
||||
className={cn(
|
||||
"mb-16 mt-4 font-poppins text-2xl font-medium text-violet-800 transition-opacity duration-500",
|
||||
showSubtext ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
>
|
||||
You earned 10$ for running your first agent
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +1,20 @@
|
||||
"use client";
|
||||
import { UserOnboarding } from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
type OnboardingState = {
|
||||
step: number;
|
||||
usageReason?: string;
|
||||
integrations: string[];
|
||||
otherIntegrations?: string;
|
||||
chosenAgentId?: string;
|
||||
agentInput?: { [key: string]: string };
|
||||
};
|
||||
|
||||
const OnboardingContext = createContext<
|
||||
| {
|
||||
state: OnboardingState;
|
||||
setState: (state: Partial<OnboardingState>) => void;
|
||||
state: UserOnboarding | null;
|
||||
setState: (state: Partial<UserOnboarding>) => void;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
@@ -27,7 +22,7 @@ const OnboardingContext = createContext<
|
||||
export function useOnboarding(step?: number) {
|
||||
const context = useContext(OnboardingContext);
|
||||
if (!context)
|
||||
throw new Error("useOnboarding must be used within OnboardingLayout");
|
||||
throw new Error("useOnboarding must be used within /onboarding pages");
|
||||
|
||||
useEffect(() => {
|
||||
if (!step) return;
|
||||
@@ -43,14 +38,57 @@ export default function OnboardingLayout({
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [state, setStateRaw] = useState<OnboardingState>({
|
||||
step: 0,
|
||||
integrations: [],
|
||||
});
|
||||
const [state, setStateRaw] = useState<UserOnboarding | null>(null);
|
||||
const api = useBackendAPI();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
const setState = (newState: Partial<OnboardingState>) => {
|
||||
setStateRaw((prev) => ({ ...prev, ...newState }));
|
||||
};
|
||||
useEffect(() => {
|
||||
const fetchOnboarding = async () => {
|
||||
const onboarding = await api.getUserOnboarding();
|
||||
setStateRaw(onboarding);
|
||||
|
||||
// Redirect outside onboarding if completed
|
||||
if (onboarding.isCompleted && !pathname.startsWith("/onboarding/reset")) {
|
||||
router.push("/library");
|
||||
}
|
||||
};
|
||||
fetchOnboarding();
|
||||
}, [api, pathname, router]);
|
||||
|
||||
const setState = useCallback(
|
||||
(newState: Partial<UserOnboarding>) => {
|
||||
function removeNullFields<T extends object>(obj: T): Partial<T> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj).filter(([_, value]) => value != null),
|
||||
) as Partial<T>;
|
||||
}
|
||||
|
||||
const updateState = (state: Partial<UserOnboarding>) => {
|
||||
if (!state) return;
|
||||
|
||||
api.updateUserOnboarding(state);
|
||||
};
|
||||
|
||||
setStateRaw((prev) => {
|
||||
if (newState.step && prev && prev?.step !== newState.step) {
|
||||
updateState(removeNullFields({ ...prev, ...newState }));
|
||||
}
|
||||
|
||||
if (!prev) {
|
||||
// Handle initial state
|
||||
return {
|
||||
step: 1,
|
||||
integrations: [],
|
||||
isCompleted: false,
|
||||
...newState,
|
||||
};
|
||||
}
|
||||
return { ...prev, ...newState };
|
||||
});
|
||||
},
|
||||
[api, setStateRaw],
|
||||
);
|
||||
|
||||
return (
|
||||
<OnboardingContext.Provider value={{ state, setState }}>
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function OnboardingPage() {
|
||||
redirect("/onboarding/1-welcome");
|
||||
export default async function OnboardingPage() {
|
||||
const api = new BackendAPI();
|
||||
const onboarding = await api.getUserOnboarding();
|
||||
|
||||
switch (onboarding.step) {
|
||||
case 1:
|
||||
redirect("/onboarding/1-welcome");
|
||||
case 2:
|
||||
redirect("/onboarding/2-reason");
|
||||
case 3:
|
||||
redirect("/onboarding/3-services");
|
||||
case 4:
|
||||
redirect("/onboarding/4-agent");
|
||||
case 5:
|
||||
redirect("/onboarding/5-run");
|
||||
default:
|
||||
redirect("/onboarding/1-welcome");
|
||||
}
|
||||
}
|
||||
|
||||
17
autogpt_platform/frontend/src/app/onboarding/reset/page.ts
Normal file
17
autogpt_platform/frontend/src/app/onboarding/reset/page.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function OnboardingResetPage() {
|
||||
const api = new BackendAPI();
|
||||
await api.updateUserOnboarding({
|
||||
step: 1,
|
||||
usageReason: undefined,
|
||||
integrations: [],
|
||||
otherIntegrations: undefined,
|
||||
selectedAgentCreator: undefined,
|
||||
selectedAgentSlug: undefined,
|
||||
agentInput: undefined,
|
||||
isCompleted: false,
|
||||
});
|
||||
redirect("/onboarding/1-welcome");
|
||||
}
|
||||
@@ -36,8 +36,8 @@ export async function signup(values: z.infer<typeof signupFormSchema>) {
|
||||
if (data.session) {
|
||||
await supabase.auth.setSession(data.session);
|
||||
}
|
||||
revalidatePath("/", "layout");
|
||||
redirect("/");
|
||||
revalidatePath("/onboarding", "layout");
|
||||
redirect("/onboarding");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import StarRating from "./StarRating";
|
||||
import { StoreAgentDetails } from "@/lib/autogpt-server-api";
|
||||
|
||||
interface OnboardingAgentCardProps {
|
||||
id: string;
|
||||
image: string;
|
||||
name: string;
|
||||
description: string;
|
||||
author: string;
|
||||
runs: number;
|
||||
rating: number;
|
||||
type OnboardingAgentCardProps = StoreAgentDetails & {
|
||||
selected?: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
};
|
||||
|
||||
export default function OnboardingAgentCard({
|
||||
id,
|
||||
image,
|
||||
name,
|
||||
agent_image,
|
||||
creator_avatar,
|
||||
agent_name,
|
||||
description,
|
||||
author,
|
||||
creator,
|
||||
runs,
|
||||
rating,
|
||||
selected,
|
||||
@@ -29,7 +23,7 @@ export default function OnboardingAgentCard({
|
||||
<div
|
||||
className={cn(
|
||||
"relative cursor-pointer transition-all duration-200 ease-in-out",
|
||||
"h-[394px] w-[368px] rounded-xl border border-transparent bg-white",
|
||||
"h-[394px] w-[368px] rounded-[20px] border border-transparent bg-white",
|
||||
selected ? "bg-[#F5F3FF80]" : "hover:border-zinc-400",
|
||||
)}
|
||||
onClick={onClick}
|
||||
@@ -37,16 +31,16 @@ export default function OnboardingAgentCard({
|
||||
{/* Image container */}
|
||||
<div className="relative">
|
||||
<Image
|
||||
src={image}
|
||||
src={agent_image?.[0] || ""}
|
||||
alt="Agent cover"
|
||||
className="m-2 h-[196px] w-[350px] rounded-xl object-cover"
|
||||
className="m-2 h-[196px] w-[350px] rounded-[16px] object-cover"
|
||||
width={350}
|
||||
height={196}
|
||||
/>
|
||||
{/* Profile picture overlay */}
|
||||
<div className="absolute bottom-2 left-4">
|
||||
<Image
|
||||
src={image}
|
||||
src={creator_avatar}
|
||||
alt="Profile picture"
|
||||
className="h-[50px] w-[50px] rounded-full border border-white object-cover object-center"
|
||||
width={50}
|
||||
@@ -61,12 +55,12 @@ export default function OnboardingAgentCard({
|
||||
<div>
|
||||
{/* Title - 2 lines max */}
|
||||
<p className="text-md line-clamp-2 max-h-[50px] font-sans text-base font-medium leading-normal text-zinc-800">
|
||||
{name}
|
||||
{agent_name}
|
||||
</p>
|
||||
|
||||
{/* Author - single line with truncate */}
|
||||
<p className="truncate text-sm font-normal leading-normal text-zinc-600">
|
||||
by {author}
|
||||
by {creator}
|
||||
</p>
|
||||
|
||||
{/* Description - 3 lines max */}
|
||||
@@ -83,14 +77,14 @@ export default function OnboardingAgentCard({
|
||||
{/* Bottom stats */}
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="mt-1 font-sans text-sm font-medium text-zinc-800">
|
||||
{runs.toLocaleString("en-US")} runs
|
||||
{runs?.toLocaleString("en-US")} runs
|
||||
</span>
|
||||
<StarRating rating={rating} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 rounded-xl border-2 transition-all duration-200 ease-in-out",
|
||||
"pointer-events-none absolute inset-0 rounded-[20px] border-2 transition-all duration-200 ease-in-out",
|
||||
selected ? "border-violet-700" : "border-transparent",
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -17,9 +17,8 @@ export default function OnboardingInput({
|
||||
<input
|
||||
className={cn(
|
||||
className,
|
||||
"relative h-[50px] w-[512px] rounded-[25px] border border-transparent bg-white px-4",
|
||||
"font-poppin text-sm font-normal leading-normal text-zinc-900 placeholder:text-zinc-400",
|
||||
"transition-all duration-200 ease-in-out",
|
||||
"font-poppin relative h-[50px] w-[512px] rounded-[25px] border border-transparent bg-white px-4 text-sm font-normal leading-normal text-zinc-900",
|
||||
"transition-all duration-200 ease-in-out placeholder:text-zinc-400",
|
||||
"focus:border-transparent focus:bg-[#F5F3FF80] focus:outline-none focus:ring-2 focus:ring-violet-700",
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
|
||||
@@ -10,18 +10,7 @@ type OnboardingListElementProps = {
|
||||
onClick: (content: string) => void;
|
||||
};
|
||||
|
||||
type OnboardingListProps = {
|
||||
className?: string;
|
||||
elements: Array<{
|
||||
label: string;
|
||||
text: string;
|
||||
id: string;
|
||||
}>;
|
||||
selectedId?: string;
|
||||
onSelect: (id: string) => void;
|
||||
};
|
||||
|
||||
function OnboardingListElement({
|
||||
export function OnboardingListElement({
|
||||
label,
|
||||
text,
|
||||
selected,
|
||||
@@ -46,20 +35,18 @@ function OnboardingListElement({
|
||||
<button
|
||||
onClick={() => onClick(content)}
|
||||
className={cn(
|
||||
"relative flex h-[78px] w-[530px] items-center rounded-xl px-5 py-4",
|
||||
"border border-transparent",
|
||||
"transition-all duration-200 ease-in-out",
|
||||
"relative flex h-[78px] w-[530px] items-center rounded-xl border border-transparent px-5 py-4 transition-all duration-200 ease-in-out",
|
||||
selected ? "bg-[#F5F3FF80]" : "bg-white hover:border-zinc-400",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<div className="flex w-full flex-col items-start gap-1">
|
||||
<span className="text-sm font-medium text-zinc-700">{label}</span>
|
||||
{custom && selected ? (
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={cn(
|
||||
selected ? "text-zinc-600" : "text-zinc-400",
|
||||
"font-poppin border-0 bg-[#F5F3FF80] text-sm focus:outline-none",
|
||||
"font-poppin w-full border-0 bg-[#F5F3FF80] text-sm focus:outline-none",
|
||||
)}
|
||||
placeholder="Please specify"
|
||||
value={content}
|
||||
@@ -89,8 +76,7 @@ function OnboardingListElement({
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 rounded-xl border-2",
|
||||
"transition-all duration-200 ease-in-out",
|
||||
"pointer-events-none absolute inset-0 rounded-xl border-2 transition-all duration-200 ease-in-out",
|
||||
selected ? "border-violet-700" : "border-transparent",
|
||||
)}
|
||||
/>
|
||||
@@ -98,7 +84,18 @@ function OnboardingListElement({
|
||||
);
|
||||
}
|
||||
|
||||
export default function OnboardingList({
|
||||
type OnboardingListProps = {
|
||||
className?: string;
|
||||
elements: Array<{
|
||||
label: string;
|
||||
text: string;
|
||||
id: string;
|
||||
}>;
|
||||
selectedId?: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
};
|
||||
|
||||
function OnboardingList({
|
||||
className,
|
||||
elements,
|
||||
selectedId,
|
||||
@@ -106,7 +103,7 @@ export default function OnboardingList({
|
||||
}: OnboardingListProps) {
|
||||
const isCustom = useCallback(() => {
|
||||
return (
|
||||
selectedId !== undefined &&
|
||||
selectedId !== null &&
|
||||
!elements.some((element) => element.id === selectedId)
|
||||
);
|
||||
}, [selectedId, elements]);
|
||||
@@ -134,3 +131,5 @@ export default function OnboardingList({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default OnboardingList;
|
||||
|
||||
@@ -42,7 +42,7 @@ export function OnboardingHeader({
|
||||
>
|
||||
<div className="flex w-full items-center justify-between px-5 py-4">
|
||||
<OnboardingBackButton href={backHref} />
|
||||
<OnboardingProgress totalSteps={5} toStep={state.step - 1} />
|
||||
<OnboardingProgress totalSteps={5} toStep={(state?.step || 1) - 1} />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useMemo } from "react";
|
||||
import { FaRegStar, FaStar, FaStarHalfAlt } from "react-icons/fa";
|
||||
|
||||
export default function StarRating({
|
||||
@@ -15,23 +16,27 @@ export default function StarRating({
|
||||
starSize ??= 15;
|
||||
|
||||
// Generate array of 5 star values
|
||||
const stars = Array(5)
|
||||
.fill(0)
|
||||
.map((_, index) => {
|
||||
const difference = roundedRating - index;
|
||||
const stars = useMemo(
|
||||
() =>
|
||||
Array(5)
|
||||
.fill(0)
|
||||
.map((_, index) => {
|
||||
const difference = roundedRating - index;
|
||||
|
||||
if (difference >= 1) {
|
||||
return "full";
|
||||
} else if (difference > 0) {
|
||||
// Half star for values between 0.2 and 0.8
|
||||
return difference >= 0.8
|
||||
? "full"
|
||||
: difference >= 0.2
|
||||
? "half"
|
||||
: "empty";
|
||||
}
|
||||
return "empty";
|
||||
});
|
||||
if (difference >= 1) {
|
||||
return "full";
|
||||
} else if (difference > 0) {
|
||||
// Half star for values between 0.2 and 0.8
|
||||
return difference >= 0.8
|
||||
? "full"
|
||||
: difference >= 0.2
|
||||
? "half"
|
||||
: "empty";
|
||||
}
|
||||
return "empty";
|
||||
}),
|
||||
[roundedRating],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
TransactionHistory,
|
||||
User,
|
||||
UserPasswordCredentials,
|
||||
UserOnboarding,
|
||||
} from "./types";
|
||||
import { createBrowserClient } from "@supabase/ssr";
|
||||
import getServerSupabase from "../supabase/getServerSupabase";
|
||||
@@ -99,6 +100,9 @@ export default class BackendAPI {
|
||||
return this._request("POST", "/auth/user/email", { email });
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
///////////// CREDITS //////////////////
|
||||
////////////////////////////////////////
|
||||
getUserCredit(): Promise<{ credits: number }> {
|
||||
try {
|
||||
return this._get("/credits");
|
||||
@@ -162,6 +166,24 @@ export default class BackendAPI {
|
||||
return this._request("PATCH", "/credits");
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
/////////// ONBOARDING /////////////////
|
||||
////////////////////////////////////////
|
||||
getUserOnboarding(): Promise<UserOnboarding> {
|
||||
return this._get("/onboarding");
|
||||
}
|
||||
|
||||
updateUserOnboarding(onboarding: Partial<UserOnboarding>): Promise<void> {
|
||||
return this._request("PATCH", "/onboarding", onboarding);
|
||||
}
|
||||
|
||||
getOnboardingAgents(): Promise<StoreAgentDetails[]> {
|
||||
return this._get("/onboarding/agents");
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
/////////// GRAPHS /////////////////////
|
||||
////////////////////////////////////////
|
||||
getBlocks(): Promise<Block[]> {
|
||||
return this._get("/blocks");
|
||||
}
|
||||
|
||||
@@ -732,6 +732,17 @@ export interface RefundRequest {
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface UserOnboarding {
|
||||
step: number;
|
||||
usageReason?: string;
|
||||
integrations: string[];
|
||||
otherIntegrations?: string;
|
||||
selectedAgentCreator?: string;
|
||||
selectedAgentSlug?: string;
|
||||
agentInput?: { [key: string]: string };
|
||||
isCompleted: boolean;
|
||||
}
|
||||
|
||||
/* *** UTILITIES *** */
|
||||
|
||||
/** Use branded types for IDs -> deny mixing IDs between different object classes */
|
||||
|
||||
@@ -18,7 +18,6 @@ test.describe("Authentication", () => {
|
||||
}) => {
|
||||
await page.goto("/login");
|
||||
await loginPage.login(testUser.email, testUser.password);
|
||||
|
||||
await test.expect(page).toHaveURL("/marketplace");
|
||||
|
||||
// Click on the profile menu trigger to open popout
|
||||
|
||||
@@ -19,7 +19,7 @@ test.describe("Build", () => { //(1)!
|
||||
// Start each test with login using worker auth
|
||||
await page.goto("/login"); //(4)!
|
||||
await loginPage.login(testUser.email, testUser.password);
|
||||
await test.expect(page).toHaveURL("/"); //(5)!
|
||||
await test.expect(page).toHaveURL("/marketplace"); //(5)!
|
||||
await buildPage.navbar.clickBuildLink();
|
||||
});
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ test.describe("Monitor", () => {
|
||||
// Start each test with login using worker auth
|
||||
await page.goto("/login");
|
||||
await loginPage.login(testUser.email, testUser.password);
|
||||
await test.expect(page).toHaveURL("/");
|
||||
await test.expect(page).toHaveURL("/marketplace");
|
||||
|
||||
// add a test agent
|
||||
const basicBlock = await buildPage.getDictionaryBlockDetails();
|
||||
|
||||
@@ -34,7 +34,11 @@ export class LoginPage {
|
||||
await loginButton.waitFor({ state: "visible" });
|
||||
|
||||
// Start waiting for navigation before clicking
|
||||
const navigationPromise = this.page.waitForURL("/", { timeout: 10_000 });
|
||||
const navigationPromise = Promise.race([
|
||||
this.page.waitForURL("/", { timeout: 10_000 }), // Wait for home page
|
||||
this.page.waitForURL("/marketplace", { timeout: 10_000 }), // Wait for home page
|
||||
this.page.waitForURL("/onboarding/**", { timeout: 10_000 }), // Wait for onboarding page
|
||||
]);
|
||||
|
||||
console.log("About to click login button"); // Debug log
|
||||
await loginButton.click();
|
||||
@@ -42,6 +46,12 @@ export class LoginPage {
|
||||
console.log("Waiting for navigation"); // Debug log
|
||||
await navigationPromise;
|
||||
|
||||
// If the user is redirected to onboarding, manually redirect to /marketplace
|
||||
if (this.page.url().includes("/onboarding")) {
|
||||
console.log("Redirecting to /marketplace"); // Debug log
|
||||
await this.page.goto("/marketplace");
|
||||
}
|
||||
|
||||
console.log("Navigation complete, waiting for network idle"); // Debug log
|
||||
await this.page.waitForLoadState("load", { timeout: 10_000 });
|
||||
console.log("Login process complete"); // Debug log
|
||||
|
||||
@@ -126,10 +126,15 @@ const config = {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
"fade-in": {
|
||||
"0%": { opacity: "0" }, // Start with opacity 0
|
||||
"100%": { opacity: "1" }, // End with opacity 1
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
"fade-in": "fade-in 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3237,10 +3237,10 @@
|
||||
"@storybook/react-dom-shim" "8.5.3"
|
||||
"@storybook/theming" "8.5.3"
|
||||
|
||||
"@storybook/test-runner@^0.20.1":
|
||||
version "0.20.1"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/test-runner/-/test-runner-0.20.1.tgz#e2efa6266d512312a6b810db376da2919008cccd"
|
||||
integrity sha512-3WU/th/uncIR6vpQDK9hKjiZjmczsluoLbgkRV7ufxY9IgHCGcbIjvT5EPS+XZIaOrNGjaPsyB5cE1okKn9iSA==
|
||||
"@storybook/test-runner@^0.21.0":
|
||||
version "0.21.3"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/test-runner/-/test-runner-0.21.3.tgz#20ad7a9ee20811c51f332557e362733df2c31c4b"
|
||||
integrity sha512-tsorR0IVYElcaTZrT/ZrH/8YDw7c+wrLDN0e3qj1WdbqU8l2IU4+a32HLXaakzdDpuTn+d8xCk4VZMlzMrmToA==
|
||||
dependencies:
|
||||
"@babel/core" "^7.22.5"
|
||||
"@babel/generator" "^7.22.5"
|
||||
@@ -3526,6 +3526,11 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.20.7"
|
||||
|
||||
"@types/canvas-confetti@^1.9.0":
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz#d1077752e046413c9881fbb2ba34a70ebe3c1773"
|
||||
integrity sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==
|
||||
|
||||
"@types/connect@3.4.36":
|
||||
version "3.4.36"
|
||||
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.36.tgz#e511558c15a39cb29bd5357eebb57bd1459cd1ab"
|
||||
@@ -4932,6 +4937,11 @@ caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001688:
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001697.tgz#040bbbb54463c4b4b3377c716b34a322d16e6fc7"
|
||||
integrity sha512-GwNPlWJin8E+d7Gxq96jxM6w0w+VFeyyXRsjU58emtkYqnbwHqXm5uT2uCmO0RQE9htWknOP4xtBlLmM/gWxvQ==
|
||||
|
||||
canvas-confetti@^1.9.3:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/canvas-confetti/-/canvas-confetti-1.9.3.tgz#ef4c857420ad8045ab4abe8547261c8cdf229845"
|
||||
integrity sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==
|
||||
|
||||
case-sensitive-paths-webpack-plugin@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4"
|
||||
|
||||
Reference in New Issue
Block a user