mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(platform): Onboarding updates (#9636)
This is a follow up to https://github.com/Significant-Gravitas/AutoGPT/pull/9511 fixing some issues and updating onboarding. ### Changes 🏗️ - Update `UserOnboarding` data - Update schema and add migration - Change `step` in `UserOnboarding` to `completedSteps` array with `OnboardingStep` enum - Remove `isCompleted`: this is now inferred from `completedSteps` values - Don't onboard if <2 marketplace agents; that prevents self-host onboarding - Add endpoints: - `is_onboarding_enabled`: to check if users should be onboarded (not if they finished onboarding); now check if there are at least 2 marketplace agents - `get_store_agent`: returns `StoreAgentDetails` for given `store_listing_version_id` - `get_graph_meta_by_store_listing_version_id`: returns `GraphMeta` - Add agent to Library just before running it and not when chosen and remove code that was responsible for removing agent that wasn't run - Move onboarding to `OnboardingProvider` (it'll be needed globally for Phase 2) - Multiple fixes, renames for clarity ### Checklist 📋 - [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] Don't onboard if less than 2 marketplace agents - [x] Avoid non-input and credentials agents - [x] Onboarding works and can be finished - [x] Onboarding resumes - [x] Onboarding agent runs correctly --------- Co-authored-by: Nicholas Tindle <nicholas.tindle@agpt.co>
This commit is contained in:
committed by
GitHub
parent
b7ca8d9c30
commit
58bb4f92b7
@@ -4,16 +4,13 @@ from typing import Any, Optional
|
||||
import prisma
|
||||
import pydantic
|
||||
from prisma import Json
|
||||
from prisma.models import (
|
||||
AgentGraph,
|
||||
AgentGraphExecution,
|
||||
StoreListingVersion,
|
||||
UserOnboarding,
|
||||
)
|
||||
from prisma.enums import OnboardingStep
|
||||
from prisma.models import 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.data.block import get_blocks
|
||||
from backend.data.graph import GraphModel
|
||||
from backend.data.model import CredentialsMetaInput
|
||||
from backend.server.v2.store.model import StoreAgentDetails
|
||||
|
||||
# Mapping from user reason id to categories to search for when choosing agent to show
|
||||
@@ -24,17 +21,17 @@ REASON_MAPPING: dict[str, list[str]] = {
|
||||
"ai_innovation": ["development", "research"],
|
||||
"personal_productivity": ["personal", "productivity"],
|
||||
}
|
||||
POINTS_AGENT_COUNT = 50 # Number of agents to calculate points for
|
||||
MIN_AGENT_COUNT = 2 # Minimum number of marketplace agents to enable onboarding
|
||||
|
||||
|
||||
class UserOnboardingUpdate(pydantic.BaseModel):
|
||||
step: int
|
||||
completedSteps: Optional[list[OnboardingStep]] = None
|
||||
usageReason: Optional[str] = None
|
||||
integrations: list[str] = pydantic.Field(default_factory=list)
|
||||
integrations: Optional[list[str]] = None
|
||||
otherIntegrations: Optional[str] = None
|
||||
selectedAgentCreator: Optional[str] = None
|
||||
selectedAgentSlug: Optional[str] = None
|
||||
selectedStoreListingVersionId: Optional[str] = None
|
||||
agentInput: Optional[dict[str, Any]] = None
|
||||
isCompleted: bool = False
|
||||
|
||||
|
||||
async def get_user_onboarding(user_id: str):
|
||||
@@ -48,50 +45,18 @@ async def get_user_onboarding(user_id: str):
|
||||
|
||||
|
||||
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: UserOnboardingUpdateInput = {}
|
||||
if data.completedSteps is not None:
|
||||
update["completedSteps"] = list(set(data.completedSteps))
|
||||
if data.usageReason is not None:
|
||||
update["usageReason"] = data.usageReason
|
||||
if data.integrations:
|
||||
if data.integrations is not None:
|
||||
update["integrations"] = data.integrations
|
||||
if data.otherIntegrations:
|
||||
if data.otherIntegrations is not None:
|
||||
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:
|
||||
if data.selectedStoreListingVersionId is not None:
|
||||
update["selectedStoreListingVersionId"] = data.selectedStoreListingVersionId
|
||||
if data.agentInput is not None:
|
||||
update["agentInput"] = Json(data.agentInput)
|
||||
|
||||
return await UserOnboarding.prisma().upsert(
|
||||
@@ -170,6 +135,20 @@ def calculate_points(
|
||||
return int(points)
|
||||
|
||||
|
||||
def get_credentials_blocks() -> dict[str, str]:
|
||||
# Returns a dictionary of block id to credentials field name
|
||||
creds: dict[str, str] = {}
|
||||
blocks = get_blocks()
|
||||
for id, block in blocks.items():
|
||||
for field_name, field_info in block().input_schema.model_fields.items():
|
||||
if field_info.annotation == CredentialsMetaInput:
|
||||
creds[id] = field_name
|
||||
return creds
|
||||
|
||||
|
||||
CREDENTIALS_FIELDS: dict[str, str] = get_credentials_blocks()
|
||||
|
||||
|
||||
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 "", [])
|
||||
@@ -193,31 +172,74 @@ async def get_recommended_agents(user_id: str) -> list[StoreAgentDetails]:
|
||||
for word in user_onboarding.integrations
|
||||
]
|
||||
|
||||
agents = await prisma.models.StoreAgent.prisma().find_many(
|
||||
storeAgents = await prisma.models.StoreAgent.prisma().find_many(
|
||||
where=prisma.types.StoreAgentWhereInput(**where_clause),
|
||||
order=[
|
||||
{"featured": "desc"},
|
||||
{"runs": "desc"},
|
||||
{"rating": "desc"},
|
||||
],
|
||||
take=100,
|
||||
)
|
||||
|
||||
if len(agents) < 2:
|
||||
agents += await prisma.models.StoreAgent.prisma().find_many(
|
||||
agentListings = await prisma.models.StoreListingVersion.prisma().find_many(
|
||||
where={
|
||||
"id": {"in": [agent.storeListingVersionId for agent in storeAgents]},
|
||||
},
|
||||
include={"Agent": True},
|
||||
)
|
||||
|
||||
for listing in agentListings:
|
||||
agent = listing.Agent
|
||||
if agent is None:
|
||||
continue
|
||||
graph = GraphModel.from_db(agent)
|
||||
# Remove agents with empty input schema
|
||||
if not graph.input_schema:
|
||||
storeAgents = [
|
||||
a for a in storeAgents if a.storeListingVersionId != listing.id
|
||||
]
|
||||
continue
|
||||
|
||||
# Remove agents with empty credentials
|
||||
# Get nodes from this agent that have credentials
|
||||
nodes = await prisma.models.AgentNode.prisma().find_many(
|
||||
where={
|
||||
"listing_id": {"not_in": [agent.listing_id for agent in agents]},
|
||||
"agentGraphId": agent.id,
|
||||
"agentBlockId": {"in": list(CREDENTIALS_FIELDS.keys())},
|
||||
},
|
||||
)
|
||||
for node in nodes:
|
||||
block_id = node.agentBlockId
|
||||
field_name = CREDENTIALS_FIELDS[block_id]
|
||||
# If there are no credentials or they are empty, remove the agent
|
||||
# FIXME ignores default values
|
||||
if (
|
||||
field_name not in node.constantInput
|
||||
or node.constantInput[field_name] is None
|
||||
):
|
||||
storeAgents = [
|
||||
a for a in storeAgents if a.storeListingVersionId != listing.id
|
||||
]
|
||||
break
|
||||
|
||||
# If there are less than 2 agents, add more agents to the list
|
||||
if len(storeAgents) < 2:
|
||||
storeAgents += await prisma.models.StoreAgent.prisma().find_many(
|
||||
where={
|
||||
"listing_id": {"not_in": [agent.listing_id for agent in storeAgents]},
|
||||
},
|
||||
order=[
|
||||
{"featured": "desc"},
|
||||
{"runs": "desc"},
|
||||
{"rating": "desc"},
|
||||
],
|
||||
take=2 - len(agents),
|
||||
take=2 - len(storeAgents),
|
||||
)
|
||||
|
||||
# Calculate points for the first 30 agents and choose the top 2
|
||||
# Calculate points for the first X agents and choose the top 2
|
||||
agent_points = []
|
||||
for agent in agents[:50]:
|
||||
for agent in storeAgents[:POINTS_AGENT_COUNT]:
|
||||
points = calculate_points(
|
||||
agent, categories, custom, user_onboarding.integrations
|
||||
)
|
||||
@@ -245,3 +267,10 @@ async def get_recommended_agents(user_id: str) -> list[StoreAgentDetails]:
|
||||
)
|
||||
for agent in recommended_agents
|
||||
]
|
||||
|
||||
|
||||
async def onboarding_enabled() -> bool:
|
||||
count = await prisma.models.StoreAgent.prisma().count(take=MIN_AGENT_COUNT + 1)
|
||||
|
||||
# Onboading is enabled if there are at least 2 agents in the store
|
||||
return count >= MIN_AGENT_COUNT
|
||||
|
||||
@@ -46,6 +46,7 @@ from backend.data.onboarding import (
|
||||
UserOnboardingUpdate,
|
||||
get_recommended_agents,
|
||||
get_user_onboarding,
|
||||
onboarding_enabled,
|
||||
update_user_onboarding,
|
||||
)
|
||||
from backend.data.user import (
|
||||
@@ -189,6 +190,15 @@ async def get_onboarding_agents(
|
||||
return await get_recommended_agents(user_id)
|
||||
|
||||
|
||||
@v1_router.get(
|
||||
"/onboarding/enabled",
|
||||
tags=["onboarding", "public"],
|
||||
dependencies=[Depends(auth_middleware)],
|
||||
)
|
||||
async def is_onboarding_enabled():
|
||||
return await onboarding_enabled()
|
||||
|
||||
|
||||
########################################################
|
||||
##################### Blocks ###########################
|
||||
########################################################
|
||||
|
||||
@@ -371,7 +371,7 @@ async def add_store_agent_to_library(
|
||||
)
|
||||
|
||||
try:
|
||||
async with locked_transaction(f"user_trx_{user_id}"):
|
||||
async with locked_transaction(f"add_agent_trx_{user_id}"):
|
||||
store_listing_version = (
|
||||
await prisma.models.StoreListingVersion.prisma().find_unique(
|
||||
where={"id": store_listing_version_id}, include={"Agent": True}
|
||||
|
||||
@@ -188,6 +188,90 @@ async def get_store_agent_details(
|
||||
) from e
|
||||
|
||||
|
||||
async def get_available_graph(
|
||||
store_listing_version_id: str,
|
||||
):
|
||||
try:
|
||||
# Get avaialble, non-deleted store listing version
|
||||
store_listing_version = (
|
||||
await prisma.models.StoreListingVersion.prisma().find_first(
|
||||
where={
|
||||
"id": store_listing_version_id,
|
||||
"isAvailable": True,
|
||||
"isDeleted": False,
|
||||
},
|
||||
include={"Agent": {"include": {"AgentNodes": True}}},
|
||||
)
|
||||
)
|
||||
|
||||
if not store_listing_version or not store_listing_version.Agent:
|
||||
raise fastapi.HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Store listing version {store_listing_version_id} not found",
|
||||
)
|
||||
|
||||
graph = GraphModel.from_db(store_listing_version.Agent)
|
||||
# We return graph meta, without nodes, they cannot be just removed
|
||||
# because then input_schema would be empty
|
||||
return {
|
||||
"id": graph.id,
|
||||
"version": graph.version,
|
||||
"is_active": graph.is_active,
|
||||
"name": graph.name,
|
||||
"description": graph.description,
|
||||
"input_schema": graph.input_schema,
|
||||
"output_schema": graph.output_schema,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting agent: {e}")
|
||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Failed to fetch agent"
|
||||
) from e
|
||||
|
||||
|
||||
async def get_store_agent_by_version_id(
|
||||
store_listing_version_id: str,
|
||||
) -> backend.server.v2.store.model.StoreAgentDetails:
|
||||
logger.debug(f"Getting store agent details for {store_listing_version_id}")
|
||||
|
||||
try:
|
||||
agent = await prisma.models.StoreAgent.prisma().find_first(
|
||||
where={"storeListingVersionId": store_listing_version_id}
|
||||
)
|
||||
|
||||
if not agent:
|
||||
logger.warning(f"Agent not found: {store_listing_version_id}")
|
||||
raise backend.server.v2.store.exceptions.AgentNotFoundError(
|
||||
f"Agent {store_listing_version_id} not found"
|
||||
)
|
||||
|
||||
logger.debug(f"Found agent details for {store_listing_version_id}")
|
||||
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: {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,
|
||||
|
||||
@@ -196,6 +196,55 @@ async def get_agent(username: str, agent_name: str):
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/graph/{store_listing_version_id}",
|
||||
tags=["store"],
|
||||
)
|
||||
async def get_graph_meta_by_store_listing_version_id(
|
||||
store_listing_version_id: str,
|
||||
_: typing.Annotated[str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)],
|
||||
):
|
||||
"""
|
||||
Get Agent Graph from Store Listing Version ID.
|
||||
"""
|
||||
try:
|
||||
graph = await backend.server.v2.store.db.get_available_graph(
|
||||
store_listing_version_id
|
||||
)
|
||||
return graph
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting agent graph")
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while retrieving the agent graph"},
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/agents/{store_listing_version_id}",
|
||||
tags=["store"],
|
||||
response_model=backend.server.v2.store.model.StoreAgentDetails,
|
||||
)
|
||||
async def get_store_agent(
|
||||
store_listing_version_id: str,
|
||||
_: typing.Annotated[str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)],
|
||||
):
|
||||
"""
|
||||
Get Store Agent Details from Store Listing Version ID.
|
||||
"""
|
||||
try:
|
||||
agent = await backend.server.v2.store.db.get_store_agent_by_version_id(
|
||||
store_listing_version_id
|
||||
)
|
||||
return agent
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting store agent")
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while retrieving the store agent"},
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/agents/{username}/{agent_name}/review",
|
||||
tags=["store"],
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
-- Create OnboardingStep enum
|
||||
CREATE TYPE "OnboardingStep" AS ENUM (
|
||||
'WELCOME',
|
||||
'USAGE_REASON',
|
||||
'INTEGRATIONS',
|
||||
'AGENT_CHOICE',
|
||||
'AGENT_NEW_RUN',
|
||||
'AGENT_INPUT',
|
||||
'CONGRATS'
|
||||
);
|
||||
|
||||
-- Modify the UserOnboarding table
|
||||
ALTER TABLE "UserOnboarding"
|
||||
DROP COLUMN "step",
|
||||
DROP COLUMN "isCompleted",
|
||||
DROP COLUMN "selectedAgentCreator",
|
||||
DROP COLUMN "selectedAgentSlug",
|
||||
ADD COLUMN "completedSteps" "OnboardingStep"[] DEFAULT '{}',
|
||||
ADD COLUMN "selectedStoreListingVersionId" TEXT
|
||||
@@ -57,18 +57,26 @@ model User {
|
||||
@@index([email])
|
||||
}
|
||||
|
||||
enum OnboardingStep {
|
||||
WELCOME
|
||||
USAGE_REASON
|
||||
INTEGRATIONS
|
||||
AGENT_CHOICE
|
||||
AGENT_NEW_RUN
|
||||
AGENT_INPUT
|
||||
CONGRATS
|
||||
}
|
||||
|
||||
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)
|
||||
completedSteps OnboardingStep[] @default([])
|
||||
usageReason String?
|
||||
integrations String[] @default([])
|
||||
otherIntegrations String?
|
||||
selectedStoreListingVersionId String?
|
||||
agentInput Json?
|
||||
|
||||
userId String @unique
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@ -31,6 +31,14 @@ export async function logout() {
|
||||
);
|
||||
}
|
||||
|
||||
async function shouldShowOnboarding() {
|
||||
const api = new BackendAPI();
|
||||
return (
|
||||
!(await api.isOnboardingEnabled()) ||
|
||||
(await api.getUserOnboarding()).completedSteps.includes("CONGRATS")
|
||||
);
|
||||
}
|
||||
|
||||
export async function login(values: z.infer<typeof loginFormSchema>) {
|
||||
return await Sentry.withServerActionInstrumentation("login", {}, async () => {
|
||||
const supabase = getServerSupabase();
|
||||
@@ -49,7 +57,8 @@ export async function login(values: z.infer<typeof loginFormSchema>) {
|
||||
}
|
||||
|
||||
await api.createUser();
|
||||
if (!(await api.getUserOnboarding()).isCompleted) {
|
||||
// Don't onboard if disabled or already onboarded
|
||||
if (await shouldShowOnboarding()) {
|
||||
revalidatePath("/onboarding", "layout");
|
||||
redirect("/onboarding");
|
||||
}
|
||||
@@ -89,7 +98,8 @@ export async function providerLogin(provider: LoginProvider) {
|
||||
}
|
||||
|
||||
await api.createUser();
|
||||
if (!(await api.getUserOnboarding()).isCompleted) {
|
||||
// Don't onboard if disabled or already onboarded
|
||||
if (await shouldShowOnboarding()) {
|
||||
revalidatePath("/onboarding", "layout");
|
||||
redirect("/onboarding");
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"use client";
|
||||
import { OnboardingText } from "@/components/onboarding/OnboardingText";
|
||||
import { useOnboarding } from "../layout";
|
||||
import OnboardingButton from "@/components/onboarding/OnboardingButton";
|
||||
import Image from "next/image";
|
||||
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
|
||||
|
||||
export default function Page() {
|
||||
// Just set step to 1
|
||||
useOnboarding(1);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
import OnboardingButton from "@/components/onboarding/OnboardingButton";
|
||||
import { useOnboarding } from "../layout";
|
||||
import {
|
||||
OnboardingFooter,
|
||||
OnboardingHeader,
|
||||
@@ -8,6 +7,8 @@ import {
|
||||
} from "@/components/onboarding/OnboardingStep";
|
||||
import { OnboardingText } from "@/components/onboarding/OnboardingText";
|
||||
import OnboardingList from "@/components/onboarding/OnboardingList";
|
||||
import { isEmptyOrWhitespace } from "@/lib/utils";
|
||||
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
|
||||
|
||||
const reasons = [
|
||||
{
|
||||
@@ -37,12 +38,8 @@ const reasons = [
|
||||
},
|
||||
];
|
||||
|
||||
function isEmptyOrWhitespace(str: string | undefined | null): boolean {
|
||||
return !str || str.trim().length === 0;
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const { state, setState } = useOnboarding(2);
|
||||
const { state, updateState } = useOnboarding(2, "WELCOME");
|
||||
|
||||
return (
|
||||
<OnboardingStep>
|
||||
@@ -57,7 +54,7 @@ export default function Page() {
|
||||
<OnboardingList
|
||||
elements={reasons}
|
||||
selectedId={state?.usageReason}
|
||||
onSelect={(usageReason) => setState({ usageReason })}
|
||||
onSelect={(usageReason) => updateState({ usageReason })}
|
||||
/>
|
||||
<OnboardingFooter>
|
||||
<OnboardingButton
|
||||
|
||||
@@ -6,10 +6,11 @@ import {
|
||||
OnboardingFooter,
|
||||
} from "@/components/onboarding/OnboardingStep";
|
||||
import { OnboardingText } from "@/components/onboarding/OnboardingText";
|
||||
import { useOnboarding } from "../layout";
|
||||
import { OnboardingGrid } from "@/components/onboarding/OnboardingGrid";
|
||||
import { useCallback } from "react";
|
||||
import OnboardingInput from "@/components/onboarding/OnboardingInput";
|
||||
import { isEmptyOrWhitespace } from "@/lib/utils";
|
||||
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
|
||||
|
||||
const services = [
|
||||
{
|
||||
@@ -109,12 +110,8 @@ const services = [
|
||||
},
|
||||
];
|
||||
|
||||
function isEmptyOrWhitespace(str: string | undefined | null): boolean {
|
||||
return !str || str.trim().length === 0;
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const { state, setState } = useOnboarding(3);
|
||||
const { state, updateState } = useOnboarding(3, "USAGE_REASON");
|
||||
|
||||
const switchIntegration = useCallback(
|
||||
(name: string) => {
|
||||
@@ -126,9 +123,9 @@ export default function Page() {
|
||||
? state.integrations.filter((i) => i !== name)
|
||||
: [...state.integrations, name];
|
||||
|
||||
setState({ integrations });
|
||||
updateState({ integrations });
|
||||
},
|
||||
[state, setState],
|
||||
[state, updateState],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -161,7 +158,7 @@ export default function Page() {
|
||||
className="mb-4"
|
||||
placeholder="Others (please specify)"
|
||||
value={state?.otherIntegrations || ""}
|
||||
onChange={(otherIntegrations) => setState({ otherIntegrations })}
|
||||
onChange={(otherIntegrations) => updateState({ otherIntegrations })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
import OnboardingButton from "@/components/onboarding/OnboardingButton";
|
||||
import { useOnboarding } from "../layout";
|
||||
import {
|
||||
OnboardingFooter,
|
||||
OnboardingHeader,
|
||||
@@ -12,13 +11,11 @@ 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;
|
||||
}
|
||||
import { isEmptyOrWhitespace } from "@/lib/utils";
|
||||
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
|
||||
|
||||
export default function Page() {
|
||||
const { state, setState } = useOnboarding(4);
|
||||
const { state, updateState } = useOnboarding(4, "INTEGRATIONS");
|
||||
const [agents, setAgents] = useState<StoreAgentDetails[]>([]);
|
||||
const api = useBackendAPI();
|
||||
|
||||
@@ -34,16 +31,20 @@ export default function Page() {
|
||||
useEffect(() => {
|
||||
// Deselect agent if it's not in the list of agents
|
||||
if (
|
||||
state?.selectedAgentSlug &&
|
||||
!agents.some((agent) => agent.slug === state.selectedAgentSlug)
|
||||
state?.selectedStoreListingVersionId &&
|
||||
agents.length > 0 &&
|
||||
!agents.some(
|
||||
(agent) =>
|
||||
agent.store_listing_version_id ===
|
||||
state.selectedStoreListingVersionId,
|
||||
)
|
||||
) {
|
||||
setState({
|
||||
selectedAgentSlug: undefined,
|
||||
selectedAgentCreator: undefined,
|
||||
agentInput: undefined,
|
||||
updateState({
|
||||
selectedStoreListingVersionId: null,
|
||||
agentInput: {},
|
||||
});
|
||||
}
|
||||
}, [state?.selectedAgentSlug, setState, agents]);
|
||||
}, [state?.selectedStoreListingVersionId, updateState, agents]);
|
||||
|
||||
return (
|
||||
<OnboardingStep>
|
||||
@@ -61,13 +62,14 @@ export default function Page() {
|
||||
{...(agents[0] || {})}
|
||||
selected={
|
||||
agents[0] !== undefined
|
||||
? state?.selectedAgentSlug == agents[0]?.slug
|
||||
? state?.selectedStoreListingVersionId ==
|
||||
agents[0]?.store_listing_version_id
|
||||
: false
|
||||
}
|
||||
onClick={() =>
|
||||
setState({
|
||||
selectedAgentSlug: agents[0].slug,
|
||||
selectedAgentCreator: agents[0].creator,
|
||||
updateState({
|
||||
selectedStoreListingVersionId: agents[0].store_listing_version_id,
|
||||
agentInput: {},
|
||||
})
|
||||
}
|
||||
/>
|
||||
@@ -75,13 +77,13 @@ export default function Page() {
|
||||
{...(agents[1] || {})}
|
||||
selected={
|
||||
agents[1] !== undefined
|
||||
? state?.selectedAgentSlug == agents[1]?.slug
|
||||
? state?.selectedStoreListingVersionId ==
|
||||
agents[1]?.store_listing_version_id
|
||||
: false
|
||||
}
|
||||
onClick={() =>
|
||||
setState({
|
||||
selectedAgentSlug: agents[1].slug,
|
||||
selectedAgentCreator: agents[1].creator,
|
||||
updateState({
|
||||
selectedStoreListingVersionId: agents[1].store_listing_version_id,
|
||||
})
|
||||
}
|
||||
/>
|
||||
@@ -90,7 +92,7 @@ export default function Page() {
|
||||
<OnboardingFooter>
|
||||
<OnboardingButton
|
||||
href="/onboarding/5-run"
|
||||
disabled={isEmptyOrWhitespace(state?.selectedAgentSlug)}
|
||||
disabled={isEmptyOrWhitespace(state?.selectedStoreListingVersionId)}
|
||||
>
|
||||
Next
|
||||
</OnboardingButton>
|
||||
|
||||
@@ -5,83 +5,87 @@ import {
|
||||
OnboardingHeader,
|
||||
} from "@/components/onboarding/OnboardingStep";
|
||||
import { OnboardingText } from "@/components/onboarding/OnboardingText";
|
||||
import { useOnboarding } from "../layout";
|
||||
import StarRating from "@/components/onboarding/StarRating";
|
||||
import { Play } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import OnboardingAgentInput from "@/components/onboarding/OnboardingAgentInput";
|
||||
import Image from "next/image";
|
||||
import { LibraryAgent, StoreAgentDetails } from "@/lib/autogpt-server-api";
|
||||
import { GraphMeta, StoreAgentDetails } from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
|
||||
|
||||
export default function Page() {
|
||||
const { state, setState } = useOnboarding(5);
|
||||
const { state, updateState, setStep } = useOnboarding(
|
||||
undefined,
|
||||
"AGENT_CHOICE",
|
||||
);
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const [agent, setAgent] = useState<GraphMeta | null>(null);
|
||||
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) {
|
||||
setStep(5);
|
||||
}, [setStep]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!state?.selectedStoreListingVersionId) {
|
||||
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,
|
||||
});
|
||||
});
|
||||
.getStoreAgentByVersionId(state?.selectedStoreListingVersionId)
|
||||
.then((storeAgent) => {
|
||||
setStoreAgent(storeAgent);
|
||||
});
|
||||
}, [
|
||||
api,
|
||||
setAgent,
|
||||
setStoreAgent,
|
||||
setState,
|
||||
state?.selectedAgentCreator,
|
||||
state?.selectedAgentSlug,
|
||||
]);
|
||||
api
|
||||
.getAgentMetaByStoreListingVersionId(state?.selectedStoreListingVersionId)
|
||||
.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 || "" : "";
|
||||
},
|
||||
);
|
||||
updateState({
|
||||
agentInput: update,
|
||||
});
|
||||
console.log("setting default input", update);
|
||||
});
|
||||
}, [api, setAgent, updateState, state?.selectedStoreListingVersionId]);
|
||||
|
||||
const setAgentInput = useCallback(
|
||||
(key: string, value: string) => {
|
||||
setState({
|
||||
...state,
|
||||
updateState({
|
||||
agentInput: {
|
||||
...state?.agentInput,
|
||||
[key]: value,
|
||||
},
|
||||
});
|
||||
},
|
||||
[state, state?.agentInput, setState],
|
||||
[state?.agentInput, updateState],
|
||||
);
|
||||
|
||||
const runAgent = useCallback(() => {
|
||||
if (!agent) {
|
||||
return;
|
||||
}
|
||||
api.executeGraph(agent.agent_id, agent.agent_version, state?.agentInput);
|
||||
console.log("running with", state?.agentInput);
|
||||
api.addMarketplaceAgentToLibrary(
|
||||
storeAgent?.store_listing_version_id || "",
|
||||
);
|
||||
api.executeGraph(agent.id, agent.version, state?.agentInput || {});
|
||||
router.push("/onboarding/6-congrats");
|
||||
}, [api, agent, router]);
|
||||
}, [api, agent, router, state?.agentInput]);
|
||||
|
||||
const runYourAgent = (
|
||||
<div className="ml-[54px] w-[481px] pl-5">
|
||||
@@ -97,7 +101,13 @@ export default function Page() {
|
||||
<div
|
||||
onClick={() => {
|
||||
setShowInput(true);
|
||||
setState({ step: 6 });
|
||||
setStep(6);
|
||||
updateState({
|
||||
completedSteps: [
|
||||
...(state?.completedSteps || []),
|
||||
"AGENT_NEW_RUN",
|
||||
],
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
"mt-16 flex h-[68px] w-[330px] items-center justify-center rounded-xl border-2 border-violet-700 bg-neutral-50",
|
||||
@@ -196,7 +206,6 @@ export default function Page() {
|
||||
key={key}
|
||||
name={value.title!}
|
||||
description={value.description || ""}
|
||||
placeholder={value.placeholder || ""}
|
||||
value={state?.agentInput?.[key] || ""}
|
||||
onChange={(v) => setAgentInput(key, v)}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,10 @@ import { redirect } from "next/navigation";
|
||||
|
||||
export async function finishOnboarding() {
|
||||
const api = new BackendAPI();
|
||||
await api.updateUserOnboarding({ step: 5, isCompleted: true });
|
||||
const onboarding = await api.getUserOnboarding();
|
||||
await api.updateUserOnboarding({
|
||||
completedSteps: [...onboarding.completedSteps, "CONGRATS"],
|
||||
});
|
||||
revalidatePath("/library", "layout");
|
||||
redirect("/library");
|
||||
}
|
||||
|
||||
@@ -3,19 +3,21 @@ import { useEffect, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { finishOnboarding } from "./actions";
|
||||
import confetti from "canvas-confetti";
|
||||
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
|
||||
|
||||
export default function Page() {
|
||||
useOnboarding(7, "AGENT_INPUT");
|
||||
const [showText, setShowText] = useState(false);
|
||||
const [showSubtext, setShowSubtext] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
confetti({
|
||||
particleCount: 100,
|
||||
particleCount: 120,
|
||||
spread: 360,
|
||||
shapes: ["square", "circle"],
|
||||
scalar: 2,
|
||||
decay: 0.92,
|
||||
origin: { y: 0.4, x: 0.5 },
|
||||
decay: 0.93,
|
||||
origin: { y: 0.38, x: 0.51 },
|
||||
});
|
||||
|
||||
const timer0 = setTimeout(() => {
|
||||
@@ -61,7 +63,7 @@ export default function Page() {
|
||||
showSubtext ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
>
|
||||
You earned 10$ for running your first agent
|
||||
You earned 15$ for running your first agent
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,102 +1,15 @@
|
||||
"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";
|
||||
|
||||
const OnboardingContext = createContext<
|
||||
| {
|
||||
state: UserOnboarding | null;
|
||||
setState: (state: Partial<UserOnboarding>) => void;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
export function useOnboarding(step?: number) {
|
||||
const context = useContext(OnboardingContext);
|
||||
if (!context)
|
||||
throw new Error("useOnboarding must be used within /onboarding pages");
|
||||
|
||||
useEffect(() => {
|
||||
if (!step) return;
|
||||
|
||||
context.setState({ step });
|
||||
}, [step]);
|
||||
|
||||
return context;
|
||||
}
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export default function OnboardingLayout({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [state, setStateRaw] = useState<UserOnboarding | null>(null);
|
||||
const api = useBackendAPI();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
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 }}>
|
||||
<div className="flex min-h-screen w-full items-center justify-center bg-gray-100">
|
||||
<div className="mx-auto flex w-full flex-col items-center">
|
||||
{children}
|
||||
</div>
|
||||
<div className="flex min-h-screen w-full items-center justify-center bg-gray-100">
|
||||
<div className="mx-auto flex w-full flex-col items-center">
|
||||
{children}
|
||||
</div>
|
||||
</OnboardingContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,20 +3,27 @@ import { redirect } from "next/navigation";
|
||||
|
||||
export default async function OnboardingPage() {
|
||||
const api = new BackendAPI();
|
||||
|
||||
if (!api.isOnboardingEnabled()) {
|
||||
redirect("/marketplace");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
// CONGRATS is the last step in intro onboarding
|
||||
if (onboarding.completedSteps.includes("CONGRATS")) redirect("/marketplace");
|
||||
else if (onboarding.completedSteps.includes("AGENT_INPUT"))
|
||||
redirect("/onboarding/6-congrats");
|
||||
else if (onboarding.completedSteps.includes("AGENT_NEW_RUN"))
|
||||
redirect("/onboarding/5-run");
|
||||
else if (onboarding.completedSteps.includes("AGENT_CHOICE"))
|
||||
redirect("/onboarding/5-agent");
|
||||
else if (onboarding.completedSteps.includes("INTEGRATIONS"))
|
||||
redirect("/onboarding/4-agent");
|
||||
else if (onboarding.completedSteps.includes("USAGE_REASON"))
|
||||
redirect("/onboarding/3-services");
|
||||
else if (onboarding.completedSteps.includes("WELCOME"))
|
||||
redirect("/onboarding/2-reason");
|
||||
|
||||
redirect("/onboarding/1-welcome");
|
||||
}
|
||||
|
||||
@@ -4,14 +4,12 @@ import { redirect } from "next/navigation";
|
||||
export default async function OnboardingResetPage() {
|
||||
const api = new BackendAPI();
|
||||
await api.updateUserOnboarding({
|
||||
step: 1,
|
||||
usageReason: undefined,
|
||||
completedSteps: [],
|
||||
usageReason: null,
|
||||
integrations: [],
|
||||
otherIntegrations: undefined,
|
||||
selectedAgentCreator: undefined,
|
||||
selectedAgentSlug: undefined,
|
||||
agentInput: undefined,
|
||||
isCompleted: false,
|
||||
otherIntegrations: "",
|
||||
selectedStoreListingVersionId: null,
|
||||
agentInput: {},
|
||||
});
|
||||
redirect("/onboarding/1-welcome");
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { BackendAPIProvider } from "@/lib/autogpt-server-api/context";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import CredentialsProvider from "@/components/integrations/credentials-provider";
|
||||
import { LaunchDarklyProvider } from "@/components/feature-flag/feature-flag-provider";
|
||||
import OnboardingProvider from "@/components/onboarding/onboarding-provider";
|
||||
|
||||
export function Providers({ children, ...props }: ThemeProviderProps) {
|
||||
return (
|
||||
@@ -14,7 +15,9 @@ export function Providers({ children, ...props }: ThemeProviderProps) {
|
||||
<BackendAPIProvider>
|
||||
<CredentialsProvider>
|
||||
<LaunchDarklyProvider>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
<OnboardingProvider>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
</OnboardingProvider>
|
||||
</LaunchDarklyProvider>
|
||||
</CredentialsProvider>
|
||||
</BackendAPIProvider>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { z } from "zod";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import getServerSupabase from "@/lib/supabase/getServerSupabase";
|
||||
import { signupFormSchema } from "@/types/auth";
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
|
||||
export async function signup(values: z.infer<typeof signupFormSchema>) {
|
||||
"use server";
|
||||
@@ -36,8 +37,13 @@ export async function signup(values: z.infer<typeof signupFormSchema>) {
|
||||
if (data.session) {
|
||||
await supabase.auth.setSession(data.session);
|
||||
}
|
||||
revalidatePath("/onboarding", "layout");
|
||||
redirect("/onboarding");
|
||||
// Don't onboard if disabled
|
||||
if (await new BackendAPI().isOnboardingEnabled()) {
|
||||
revalidatePath("/onboarding", "layout");
|
||||
redirect("/onboarding");
|
||||
}
|
||||
revalidatePath("/", "layout");
|
||||
redirect("/");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ interface OnboardingAgentInputProps {
|
||||
className?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
placeholder: string;
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ReactNode } from "react";
|
||||
import OnboardingBackButton from "./OnboardingBackButton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import OnboardingProgress from "./OnboardingProgress";
|
||||
import { useOnboarding } from "@/app/onboarding/layout";
|
||||
import { useOnboarding } from "./onboarding-provider";
|
||||
|
||||
export function OnboardingStep({
|
||||
dotted,
|
||||
@@ -33,7 +33,7 @@ export function OnboardingHeader({
|
||||
transparent,
|
||||
children,
|
||||
}: OnboardingHeaderProps) {
|
||||
const { state } = useOnboarding();
|
||||
const { step } = useOnboarding();
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-10 w-full">
|
||||
@@ -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) - 1} />
|
||||
<OnboardingProgress totalSteps={5} toStep={(step || 1) - 1} />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
import { OnboardingStep, 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";
|
||||
|
||||
const OnboardingContext = createContext<
|
||||
| {
|
||||
state: UserOnboarding | null;
|
||||
updateState: (state: Partial<UserOnboarding>) => void;
|
||||
step: number;
|
||||
setStep: (step: number) => void;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
export function useOnboarding(step?: number, completeStep?: OnboardingStep) {
|
||||
const context = useContext(OnboardingContext);
|
||||
if (!context)
|
||||
throw new Error("useOnboarding must be used within an OnboardingProvider");
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!completeStep ||
|
||||
!context.state ||
|
||||
context.state.completedSteps.includes(completeStep)
|
||||
)
|
||||
return;
|
||||
|
||||
context.updateState({
|
||||
completedSteps: [...context.state.completedSteps, completeStep],
|
||||
});
|
||||
}, [completeStep, context.state, context.updateState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (step && context.step !== step) {
|
||||
context.setStep(step);
|
||||
}
|
||||
}, [step, context.step, context.setStep]);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
export default function OnboardingProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [state, setState] = useState<UserOnboarding | null>(null);
|
||||
// Step is used to control the progress bar, it's frontend only
|
||||
const [step, setStep] = useState(1);
|
||||
const api = useBackendAPI();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchOnboarding = async () => {
|
||||
const enabled = await api.isOnboardingEnabled();
|
||||
if (!enabled && pathname.startsWith("/onboarding")) {
|
||||
router.push("/marketplace");
|
||||
return;
|
||||
}
|
||||
const onboarding = await api.getUserOnboarding();
|
||||
setState((prev) => ({ ...onboarding, ...prev }));
|
||||
|
||||
// Redirect outside onboarding if completed
|
||||
// If user did CONGRATS step, that means they completed introductory onboarding
|
||||
if (
|
||||
onboarding.completedSteps.includes("CONGRATS") &&
|
||||
pathname.startsWith("/onboarding") &&
|
||||
!pathname.startsWith("/onboarding/reset")
|
||||
) {
|
||||
router.push("/marketplace");
|
||||
}
|
||||
};
|
||||
fetchOnboarding();
|
||||
}, [api, pathname, router]);
|
||||
|
||||
const updateState = useCallback(
|
||||
(newState: Partial<UserOnboarding>) => {
|
||||
setState((prev) => {
|
||||
api.updateUserOnboarding({ ...prev, ...newState });
|
||||
|
||||
if (!prev) {
|
||||
// Handle initial state
|
||||
return {
|
||||
completedSteps: [],
|
||||
usageReason: null,
|
||||
integrations: [],
|
||||
otherIntegrations: null,
|
||||
selectedStoreListingVersionId: null,
|
||||
agentInput: null,
|
||||
...newState,
|
||||
};
|
||||
}
|
||||
return { ...prev, ...newState };
|
||||
});
|
||||
},
|
||||
[api, setState],
|
||||
);
|
||||
|
||||
return (
|
||||
<OnboardingContext.Provider value={{ state, updateState, step, setStep }}>
|
||||
{children}
|
||||
</OnboardingContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -186,6 +186,11 @@ export default class BackendAPI {
|
||||
return this._get("/onboarding/agents");
|
||||
}
|
||||
|
||||
/** Check if onboarding is enabled not if user finished it or not. */
|
||||
isOnboardingEnabled(): Promise<boolean> {
|
||||
return this._get("/onboarding/enabled");
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
/////////// GRAPHS /////////////////////
|
||||
////////////////////////////////////////
|
||||
@@ -430,6 +435,18 @@ export default class BackendAPI {
|
||||
);
|
||||
}
|
||||
|
||||
getAgentMetaByStoreListingVersionId(
|
||||
storeListingVersionID: string,
|
||||
): Promise<GraphMeta> {
|
||||
return this._get(`/store/graph/${storeListingVersionID}`);
|
||||
}
|
||||
|
||||
getStoreAgentByVersionId(
|
||||
storeListingVersionID: string,
|
||||
): Promise<StoreAgentDetails> {
|
||||
return this._get(`/store/agents/${storeListingVersionID}`);
|
||||
}
|
||||
|
||||
getStoreCreators(params?: {
|
||||
featured?: boolean;
|
||||
search_query?: string;
|
||||
|
||||
@@ -761,15 +761,22 @@ export interface RefundRequest {
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export type OnboardingStep =
|
||||
| "WELCOME"
|
||||
| "USAGE_REASON"
|
||||
| "INTEGRATIONS"
|
||||
| "AGENT_CHOICE"
|
||||
| "AGENT_NEW_RUN"
|
||||
| "AGENT_INPUT"
|
||||
| "CONGRATS";
|
||||
|
||||
export interface UserOnboarding {
|
||||
step: number;
|
||||
usageReason?: string;
|
||||
completedSteps: OnboardingStep[];
|
||||
usageReason: string | null;
|
||||
integrations: string[];
|
||||
otherIntegrations?: string;
|
||||
selectedAgentCreator?: string;
|
||||
selectedAgentSlug?: string;
|
||||
agentInput?: { [key: string]: string };
|
||||
isCompleted: boolean;
|
||||
otherIntegrations: string | null;
|
||||
selectedStoreListingVersionId: string | null;
|
||||
agentInput: { [key: string]: string } | null;
|
||||
}
|
||||
|
||||
/* *** UTILITIES *** */
|
||||
|
||||
@@ -390,3 +390,8 @@ export function getValue(key: string, value: any) {
|
||||
return acc[k.key];
|
||||
}, value);
|
||||
}
|
||||
|
||||
/** Check if a string is empty or whitespace */
|
||||
export function isEmptyOrWhitespace(str: string | undefined | null): boolean {
|
||||
return !str || str.trim().length === 0;
|
||||
}
|
||||
|
||||
@@ -46,11 +46,7 @@ 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");
|
||||
}
|
||||
await this.page.goto("/marketplace");
|
||||
|
||||
console.log("Navigation complete, waiting for network idle"); // Debug log
|
||||
await this.page.waitForLoadState("load", { timeout: 10_000 });
|
||||
|
||||
Reference in New Issue
Block a user