mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-08 06:44:05 -05:00
feat(platform): Backend completion of Onboarding tasks (#11375)
Make onboarding task completion backend-authoritative which prevents cheating (previously users could mark all tasks as completed instantly and get rewards) and makes task completion more reliable. Completion of tasks is moved backend with exception of introductory onboarding tasks and visit-page type tasks. ### Changes 🏗️ - Move incrementing run counter backend and make webhook-triggered and scheduled task execution count as well - Use user timezone for calculating run streak - Frontend task completion is moved from update onboarding state to separate endpoint and guarded so only frontend tasks can be completed - Graph creation, execution and add marketplace agent to library accept `source`, so appropriate tasks can be completed - Replace `client.ts` api calls with orval generated and remove no longer used functions from `client.ts` - Add `resolveResponse` helper function that unwraps orval generated call result to 2xx response Small changes&bug fixes: - Make Redis notification bus serialize all payload fields - Fix confetti when group is finished - Collapse finished group when opening Wallet - Play confetti only for tasks that are listed in the Wallet UI ### 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] Onboarding can be finished - [x] All tasks can be finished and work properly - [x] Confetti works properly
This commit is contained in:
committed by
GitHub
parent
486099140d
commit
c880db439d
@@ -22,7 +22,7 @@ from typing import (
|
||||
from urllib.parse import urlparse
|
||||
from uuid import uuid4
|
||||
|
||||
from prisma.enums import CreditTransactionType
|
||||
from prisma.enums import CreditTransactionType, OnboardingStep
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
@@ -868,3 +868,20 @@ class UserExecutionSummaryStats(BaseModel):
|
||||
total_execution_time: float = Field(default=0)
|
||||
average_execution_time: float = Field(default=0)
|
||||
cost_breakdown: dict[str, float] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class UserOnboarding(BaseModel):
|
||||
userId: str
|
||||
completedSteps: list[OnboardingStep]
|
||||
walletShown: bool
|
||||
notified: list[OnboardingStep]
|
||||
rewardedFor: list[OnboardingStep]
|
||||
usageReason: Optional[str]
|
||||
integrations: list[str]
|
||||
otherIntegrations: Optional[str]
|
||||
selectedStoreListingVersionId: Optional[str]
|
||||
agentInput: Optional[dict[str, Any]]
|
||||
onboardingAgentExecutionId: Optional[str]
|
||||
agentRuns: int
|
||||
lastRunAt: Optional[datetime]
|
||||
consecutiveRunDays: int
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, field_serializer
|
||||
|
||||
from backend.data.event_bus import AsyncRedisEventBus
|
||||
from backend.server.model import NotificationPayload
|
||||
@@ -15,6 +15,11 @@ class NotificationEvent(BaseModel):
|
||||
user_id: str
|
||||
payload: NotificationPayload
|
||||
|
||||
@field_serializer("payload")
|
||||
def serialize_payload(self, payload: NotificationPayload):
|
||||
"""Ensure extra fields survive Redis serialization."""
|
||||
return payload.model_dump()
|
||||
|
||||
|
||||
class AsyncRedisNotificationEventBus(AsyncRedisEventBus[NotificationEvent]):
|
||||
Model = NotificationEvent # type: ignore
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Literal, Optional
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import prisma
|
||||
import pydantic
|
||||
@@ -8,17 +9,18 @@ from prisma.enums import OnboardingStep
|
||||
from prisma.models import UserOnboarding
|
||||
from prisma.types import UserOnboardingCreateInput, UserOnboardingUpdateInput
|
||||
|
||||
from backend.data.block import get_blocks
|
||||
from backend.data import execution as execution_db
|
||||
from backend.data.credit import get_user_credit_model
|
||||
from backend.data.model import CredentialsMetaInput
|
||||
from backend.data.notification_bus import (
|
||||
AsyncRedisNotificationEventBus,
|
||||
NotificationEvent,
|
||||
)
|
||||
from backend.data.user import get_user_by_id
|
||||
from backend.server.model import OnboardingNotificationPayload
|
||||
from backend.server.v2.store.model import StoreAgentDetails
|
||||
from backend.util.cache import cached
|
||||
from backend.util.json import SafeJson
|
||||
from backend.util.timezone_utils import get_user_timezone_or_utc
|
||||
|
||||
# Mapping from user reason id to categories to search for when choosing agent to show
|
||||
REASON_MAPPING: dict[str, list[str]] = {
|
||||
@@ -31,9 +33,20 @@ REASON_MAPPING: dict[str, list[str]] = {
|
||||
POINTS_AGENT_COUNT = 50 # Number of agents to calculate points for
|
||||
MIN_AGENT_COUNT = 2 # Minimum number of marketplace agents to enable onboarding
|
||||
|
||||
FrontendOnboardingStep = Literal[
|
||||
OnboardingStep.WELCOME,
|
||||
OnboardingStep.USAGE_REASON,
|
||||
OnboardingStep.INTEGRATIONS,
|
||||
OnboardingStep.AGENT_CHOICE,
|
||||
OnboardingStep.AGENT_NEW_RUN,
|
||||
OnboardingStep.AGENT_INPUT,
|
||||
OnboardingStep.CONGRATS,
|
||||
OnboardingStep.MARKETPLACE_VISIT,
|
||||
OnboardingStep.BUILDER_OPEN,
|
||||
]
|
||||
|
||||
|
||||
class UserOnboardingUpdate(pydantic.BaseModel):
|
||||
completedSteps: Optional[list[OnboardingStep]] = None
|
||||
walletShown: Optional[bool] = None
|
||||
notified: Optional[list[OnboardingStep]] = None
|
||||
usageReason: Optional[str] = None
|
||||
@@ -42,9 +55,6 @@ class UserOnboardingUpdate(pydantic.BaseModel):
|
||||
selectedStoreListingVersionId: Optional[str] = None
|
||||
agentInput: Optional[dict[str, Any]] = None
|
||||
onboardingAgentExecutionId: Optional[str] = None
|
||||
agentRuns: Optional[int] = None
|
||||
lastRunAt: Optional[datetime] = None
|
||||
consecutiveRunDays: Optional[int] = None
|
||||
|
||||
|
||||
async def get_user_onboarding(user_id: str):
|
||||
@@ -83,14 +93,6 @@ async def reset_user_onboarding(user_id: str):
|
||||
async def update_user_onboarding(user_id: str, data: UserOnboardingUpdate):
|
||||
update: UserOnboardingUpdateInput = {}
|
||||
onboarding = await get_user_onboarding(user_id)
|
||||
if data.completedSteps is not None:
|
||||
update["completedSteps"] = list(
|
||||
set(data.completedSteps + onboarding.completedSteps)
|
||||
)
|
||||
for step in data.completedSteps:
|
||||
if step not in onboarding.completedSteps:
|
||||
await _reward_user(user_id, onboarding, step)
|
||||
await _send_onboarding_notification(user_id, step)
|
||||
if data.walletShown:
|
||||
update["walletShown"] = data.walletShown
|
||||
if data.notified is not None:
|
||||
@@ -107,12 +109,6 @@ async def update_user_onboarding(user_id: str, data: UserOnboardingUpdate):
|
||||
update["agentInput"] = SafeJson(data.agentInput)
|
||||
if data.onboardingAgentExecutionId is not None:
|
||||
update["onboardingAgentExecutionId"] = data.onboardingAgentExecutionId
|
||||
if data.agentRuns is not None and data.agentRuns > onboarding.agentRuns:
|
||||
update["agentRuns"] = data.agentRuns
|
||||
if data.lastRunAt is not None:
|
||||
update["lastRunAt"] = data.lastRunAt
|
||||
if data.consecutiveRunDays is not None:
|
||||
update["consecutiveRunDays"] = data.consecutiveRunDays
|
||||
|
||||
return await UserOnboarding.prisma().upsert(
|
||||
where={"userId": user_id},
|
||||
@@ -161,14 +157,12 @@ async def _reward_user(user_id: str, onboarding: UserOnboarding, step: Onboardin
|
||||
if step in onboarding.rewardedFor:
|
||||
return
|
||||
|
||||
onboarding.rewardedFor.append(step)
|
||||
user_credit_model = await get_user_credit_model(user_id)
|
||||
await user_credit_model.onboarding_reward(user_id, reward, step)
|
||||
await UserOnboarding.prisma().update(
|
||||
where={"userId": user_id},
|
||||
data={
|
||||
"completedSteps": list(set(onboarding.completedSteps + [step])),
|
||||
"rewardedFor": onboarding.rewardedFor,
|
||||
"rewardedFor": list(set(onboarding.rewardedFor + [step])),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -177,31 +171,52 @@ async def complete_onboarding_step(user_id: str, step: OnboardingStep):
|
||||
"""
|
||||
Completes the specified onboarding step for the user if not already completed.
|
||||
"""
|
||||
|
||||
onboarding = await get_user_onboarding(user_id)
|
||||
if step not in onboarding.completedSteps:
|
||||
await update_user_onboarding(
|
||||
user_id,
|
||||
UserOnboardingUpdate(completedSteps=onboarding.completedSteps + [step]),
|
||||
await UserOnboarding.prisma().update(
|
||||
where={"userId": user_id},
|
||||
data={
|
||||
"completedSteps": list(set(onboarding.completedSteps + [step])),
|
||||
},
|
||||
)
|
||||
await _reward_user(user_id, onboarding, step)
|
||||
await _send_onboarding_notification(user_id, step)
|
||||
|
||||
|
||||
async def _send_onboarding_notification(user_id: str, step: OnboardingStep):
|
||||
async def _send_onboarding_notification(
|
||||
user_id: str, step: OnboardingStep | None, event: str = "step_completed"
|
||||
):
|
||||
"""
|
||||
Sends an onboarding notification to the user for the specified step.
|
||||
Sends an onboarding notification to the user.
|
||||
"""
|
||||
payload = OnboardingNotificationPayload(
|
||||
type="onboarding",
|
||||
event="step_completed",
|
||||
step=step.value,
|
||||
event=event,
|
||||
step=step,
|
||||
)
|
||||
await AsyncRedisNotificationEventBus().publish(
|
||||
NotificationEvent(user_id=user_id, payload=payload)
|
||||
)
|
||||
|
||||
|
||||
def clean_and_split(text: str) -> list[str]:
|
||||
async def complete_re_run_agent(user_id: str, graph_id: str) -> None:
|
||||
"""
|
||||
Complete RE_RUN_AGENT step when a user runs a graph they've run before.
|
||||
Keeps overhead low by only counting executions if the step is still pending.
|
||||
"""
|
||||
onboarding = await get_user_onboarding(user_id)
|
||||
if OnboardingStep.RE_RUN_AGENT in onboarding.completedSteps:
|
||||
return
|
||||
|
||||
# Includes current execution, so count > 1 means there was at least one prior run.
|
||||
previous_exec_count = await execution_db.get_graph_executions_count(
|
||||
user_id=user_id, graph_id=graph_id
|
||||
)
|
||||
if previous_exec_count > 1:
|
||||
await complete_onboarding_step(user_id, OnboardingStep.RE_RUN_AGENT)
|
||||
|
||||
|
||||
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.
|
||||
@@ -224,7 +239,7 @@ def clean_and_split(text: str) -> list[str]:
|
||||
return words
|
||||
|
||||
|
||||
def calculate_points(
|
||||
def _calculate_points(
|
||||
agent, categories: list[str], custom: list[str], integrations: list[str]
|
||||
) -> int:
|
||||
"""
|
||||
@@ -268,18 +283,85 @@ 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
|
||||
def _normalize_datetime(value: datetime | None) -> datetime | None:
|
||||
if value is None:
|
||||
return None
|
||||
if value.tzinfo is None:
|
||||
return value.replace(tzinfo=timezone.utc)
|
||||
return value.astimezone(timezone.utc)
|
||||
|
||||
|
||||
CREDENTIALS_FIELDS: dict[str, str] = get_credentials_blocks()
|
||||
def _calculate_consecutive_run_days(
|
||||
last_run_at: datetime | None, current_consecutive_days: int, user_timezone: str
|
||||
) -> tuple[datetime, int]:
|
||||
tz = ZoneInfo(user_timezone)
|
||||
local_now = datetime.now(tz)
|
||||
normalized_last_run = _normalize_datetime(last_run_at)
|
||||
|
||||
if normalized_last_run is None:
|
||||
return local_now.astimezone(timezone.utc), 1
|
||||
|
||||
last_run_local = normalized_last_run.astimezone(tz)
|
||||
last_run_date = last_run_local.date()
|
||||
today = local_now.date()
|
||||
|
||||
if last_run_date == today:
|
||||
return local_now.astimezone(timezone.utc), current_consecutive_days
|
||||
|
||||
if last_run_date == today - timedelta(days=1):
|
||||
return local_now.astimezone(timezone.utc), current_consecutive_days + 1
|
||||
|
||||
return local_now.astimezone(timezone.utc), 1
|
||||
|
||||
|
||||
def _get_run_milestone_steps(
|
||||
new_run_count: int, consecutive_days: int
|
||||
) -> list[OnboardingStep]:
|
||||
milestones: list[OnboardingStep] = []
|
||||
if new_run_count >= 10:
|
||||
milestones.append(OnboardingStep.RUN_AGENTS)
|
||||
if new_run_count >= 100:
|
||||
milestones.append(OnboardingStep.RUN_AGENTS_100)
|
||||
if consecutive_days >= 3:
|
||||
milestones.append(OnboardingStep.RUN_3_DAYS)
|
||||
if consecutive_days >= 14:
|
||||
milestones.append(OnboardingStep.RUN_14_DAYS)
|
||||
return milestones
|
||||
|
||||
|
||||
async def _get_user_timezone(user_id: str) -> str:
|
||||
user = await get_user_by_id(user_id)
|
||||
return get_user_timezone_or_utc(user.timezone if user else None)
|
||||
|
||||
|
||||
async def increment_runs(user_id: str):
|
||||
"""
|
||||
Increment a user's run counters and trigger any onboarding milestones.
|
||||
"""
|
||||
user_timezone = await _get_user_timezone(user_id)
|
||||
onboarding = await get_user_onboarding(user_id)
|
||||
new_run_count = onboarding.agentRuns + 1
|
||||
last_run_at, consecutive_run_days = _calculate_consecutive_run_days(
|
||||
onboarding.lastRunAt, onboarding.consecutiveRunDays, user_timezone
|
||||
)
|
||||
|
||||
await UserOnboarding.prisma().update(
|
||||
where={"userId": user_id},
|
||||
data={
|
||||
"agentRuns": {"increment": 1},
|
||||
"lastRunAt": last_run_at,
|
||||
"consecutiveRunDays": consecutive_run_days,
|
||||
},
|
||||
)
|
||||
|
||||
milestones = _get_run_milestone_steps(new_run_count, consecutive_run_days)
|
||||
new_steps = [step for step in milestones if step not in onboarding.completedSteps]
|
||||
|
||||
for step in new_steps:
|
||||
await complete_onboarding_step(user_id, step)
|
||||
# Send progress notification if no steps were completed, so client refetches onboarding state
|
||||
if not new_steps:
|
||||
await _send_onboarding_notification(user_id, None, event="increment_runs")
|
||||
|
||||
|
||||
async def get_recommended_agents(user_id: str) -> list[StoreAgentDetails]:
|
||||
@@ -288,7 +370,7 @@ async def get_recommended_agents(user_id: str) -> list[StoreAgentDetails]:
|
||||
|
||||
where_clause: dict[str, Any] = {}
|
||||
|
||||
custom = clean_and_split((user_onboarding.usageReason or "").lower())
|
||||
custom = _clean_and_split((user_onboarding.usageReason or "").lower())
|
||||
|
||||
if categories:
|
||||
where_clause["OR"] = [
|
||||
@@ -336,7 +418,7 @@ async def get_recommended_agents(user_id: str) -> list[StoreAgentDetails]:
|
||||
# Calculate points for the first X agents and choose the top 2
|
||||
agent_points = []
|
||||
for agent in storeAgents[:POINTS_AGENT_COUNT]:
|
||||
points = calculate_points(
|
||||
points = _calculate_points(
|
||||
agent, categories, custom, user_onboarding.integrations
|
||||
)
|
||||
agent_points.append((agent, points))
|
||||
|
||||
@@ -26,6 +26,7 @@ from sqlalchemy import MetaData, create_engine
|
||||
from backend.data.block import BlockInput
|
||||
from backend.data.execution import GraphExecutionWithNodes
|
||||
from backend.data.model import CredentialsMetaInput
|
||||
from backend.data.onboarding import increment_runs
|
||||
from backend.executor import utils as execution_utils
|
||||
from backend.monitoring import (
|
||||
NotificationJobArgs,
|
||||
@@ -153,6 +154,7 @@ async def _execute_graph(**kwargs):
|
||||
inputs=args.input_data,
|
||||
graph_credentials_inputs=args.input_credentials,
|
||||
)
|
||||
await increment_runs(args.user_id)
|
||||
elapsed = asyncio.get_event_loop().time() - start_time
|
||||
logger.info(
|
||||
f"Graph execution started with ID {graph_exec.id} for graph {args.graph_id} "
|
||||
|
||||
@@ -33,7 +33,11 @@ from backend.data.model import (
|
||||
OAuth2Credentials,
|
||||
UserIntegrations,
|
||||
)
|
||||
from backend.data.onboarding import OnboardingStep, complete_onboarding_step
|
||||
from backend.data.onboarding import (
|
||||
OnboardingStep,
|
||||
complete_onboarding_step,
|
||||
increment_runs,
|
||||
)
|
||||
from backend.data.user import get_user_integrations
|
||||
from backend.executor.utils import add_graph_execution
|
||||
from backend.integrations.ayrshare import AyrshareClient, SocialPlatform
|
||||
@@ -377,6 +381,7 @@ async def webhook_ingress_generic(
|
||||
return
|
||||
|
||||
await complete_onboarding_step(user_id, OnboardingStep.TRIGGER_WEBHOOK)
|
||||
await increment_runs(user_id)
|
||||
|
||||
# Execute all triggers concurrently for better performance
|
||||
tasks = []
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import enum
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Literal, Optional
|
||||
|
||||
import pydantic
|
||||
from prisma.enums import OnboardingStep
|
||||
|
||||
from backend.data.api_key import APIKeyInfo, APIKeyPermission
|
||||
from backend.data.graph import Graph
|
||||
@@ -35,8 +36,13 @@ class WSSubscribeGraphExecutionsRequest(pydantic.BaseModel):
|
||||
graph_id: str
|
||||
|
||||
|
||||
GraphCreationSource = Literal["builder", "upload"]
|
||||
GraphExecutionSource = Literal["builder", "library", "onboarding"]
|
||||
|
||||
|
||||
class CreateGraph(pydantic.BaseModel):
|
||||
graph: Graph
|
||||
source: GraphCreationSource | None = None
|
||||
|
||||
|
||||
class CreateAPIKeyRequest(pydantic.BaseModel):
|
||||
@@ -83,6 +89,8 @@ class NotificationPayload(pydantic.BaseModel):
|
||||
type: str
|
||||
event: str
|
||||
|
||||
model_config = pydantic.ConfigDict(extra="allow")
|
||||
|
||||
|
||||
class OnboardingNotificationPayload(NotificationPayload):
|
||||
step: str
|
||||
step: OnboardingStep | None
|
||||
|
||||
@@ -5,7 +5,7 @@ import time
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timezone
|
||||
from typing import Annotated, Any, Sequence
|
||||
from typing import Annotated, Any, Sequence, get_args
|
||||
|
||||
import pydantic
|
||||
import stripe
|
||||
@@ -45,12 +45,17 @@ from backend.data.credit import (
|
||||
set_auto_top_up,
|
||||
)
|
||||
from backend.data.graph import GraphSettings
|
||||
from backend.data.model import CredentialsMetaInput
|
||||
from backend.data.model import CredentialsMetaInput, UserOnboarding
|
||||
from backend.data.notifications import NotificationPreference, NotificationPreferenceDTO
|
||||
from backend.data.onboarding import (
|
||||
FrontendOnboardingStep,
|
||||
OnboardingStep,
|
||||
UserOnboardingUpdate,
|
||||
complete_onboarding_step,
|
||||
complete_re_run_agent,
|
||||
get_recommended_agents,
|
||||
get_user_onboarding,
|
||||
increment_runs,
|
||||
onboarding_enabled,
|
||||
reset_user_onboarding,
|
||||
update_user_onboarding,
|
||||
@@ -78,6 +83,7 @@ from backend.server.model import (
|
||||
CreateAPIKeyRequest,
|
||||
CreateAPIKeyResponse,
|
||||
CreateGraph,
|
||||
GraphExecutionSource,
|
||||
RequestTopUp,
|
||||
SetGraphActiveVersion,
|
||||
TimezoneResponse,
|
||||
@@ -85,6 +91,7 @@ from backend.server.model import (
|
||||
UpdateTimezoneRequest,
|
||||
UploadFileResponse,
|
||||
)
|
||||
from backend.server.v2.store.model import StoreAgentDetails
|
||||
from backend.util.cache import cached
|
||||
from backend.util.clients import get_scheduler_client
|
||||
from backend.util.cloud_storage import get_cloud_storage_handler
|
||||
@@ -274,9 +281,10 @@ async def update_preferences(
|
||||
|
||||
@v1_router.get(
|
||||
"/onboarding",
|
||||
summary="Get onboarding status",
|
||||
summary="Onboarding state",
|
||||
tags=["onboarding"],
|
||||
dependencies=[Security(requires_user)],
|
||||
response_model=UserOnboarding,
|
||||
)
|
||||
async def get_onboarding(user_id: Annotated[str, Security(get_user_id)]):
|
||||
return await get_user_onboarding(user_id)
|
||||
@@ -284,9 +292,10 @@ async def get_onboarding(user_id: Annotated[str, Security(get_user_id)]):
|
||||
|
||||
@v1_router.patch(
|
||||
"/onboarding",
|
||||
summary="Update onboarding progress",
|
||||
summary="Update onboarding state",
|
||||
tags=["onboarding"],
|
||||
dependencies=[Security(requires_user)],
|
||||
response_model=UserOnboarding,
|
||||
)
|
||||
async def update_onboarding(
|
||||
user_id: Annotated[str, Security(get_user_id)], data: UserOnboardingUpdate
|
||||
@@ -294,25 +303,39 @@ async def update_onboarding(
|
||||
return await update_user_onboarding(user_id, data)
|
||||
|
||||
|
||||
@v1_router.post(
|
||||
"/onboarding/step",
|
||||
summary="Complete onboarding step",
|
||||
tags=["onboarding"],
|
||||
dependencies=[Security(requires_user)],
|
||||
)
|
||||
async def onboarding_complete_step(
|
||||
user_id: Annotated[str, Security(get_user_id)], step: FrontendOnboardingStep
|
||||
):
|
||||
if step not in get_args(FrontendOnboardingStep):
|
||||
raise HTTPException(status_code=400, detail="Invalid onboarding step")
|
||||
return await complete_onboarding_step(user_id, step)
|
||||
|
||||
|
||||
@v1_router.get(
|
||||
"/onboarding/agents",
|
||||
summary="Get recommended agents",
|
||||
summary="Recommended onboarding agents",
|
||||
tags=["onboarding"],
|
||||
dependencies=[Security(requires_user)],
|
||||
)
|
||||
async def get_onboarding_agents(
|
||||
user_id: Annotated[str, Security(get_user_id)],
|
||||
):
|
||||
) -> list[StoreAgentDetails]:
|
||||
return await get_recommended_agents(user_id)
|
||||
|
||||
|
||||
@v1_router.get(
|
||||
"/onboarding/enabled",
|
||||
summary="Check onboarding enabled",
|
||||
summary="Is onboarding enabled",
|
||||
tags=["onboarding", "public"],
|
||||
dependencies=[Security(requires_user)],
|
||||
)
|
||||
async def is_onboarding_enabled():
|
||||
async def is_onboarding_enabled() -> bool:
|
||||
return await onboarding_enabled()
|
||||
|
||||
|
||||
@@ -321,6 +344,7 @@ async def is_onboarding_enabled():
|
||||
summary="Reset onboarding progress",
|
||||
tags=["onboarding"],
|
||||
dependencies=[Security(requires_user)],
|
||||
response_model=UserOnboarding,
|
||||
)
|
||||
async def reset_onboarding(user_id: Annotated[str, Security(get_user_id)]):
|
||||
return await reset_user_onboarding(user_id)
|
||||
@@ -809,7 +833,12 @@ async def create_new_graph(
|
||||
# as the graph already valid and no sub-graphs are returned back.
|
||||
await graph_db.create_graph(graph, user_id=user_id)
|
||||
await library_db.create_library_agent(graph, user_id=user_id)
|
||||
return await on_graph_activate(graph, user_id=user_id)
|
||||
activated_graph = await on_graph_activate(graph, user_id=user_id)
|
||||
|
||||
if create_graph.source == "builder":
|
||||
await complete_onboarding_step(user_id, OnboardingStep.BUILDER_SAVE_AGENT)
|
||||
|
||||
return activated_graph
|
||||
|
||||
|
||||
@v1_router.delete(
|
||||
@@ -967,6 +996,7 @@ async def execute_graph(
|
||||
credentials_inputs: Annotated[
|
||||
dict[str, CredentialsMetaInput], Body(..., embed=True, default_factory=dict)
|
||||
],
|
||||
source: Annotated[GraphExecutionSource | None, Body(embed=True)] = None,
|
||||
graph_version: Optional[int] = None,
|
||||
preset_id: Optional[str] = None,
|
||||
) -> execution_db.GraphExecutionMeta:
|
||||
@@ -990,6 +1020,14 @@ async def execute_graph(
|
||||
# Record successful graph execution
|
||||
record_graph_execution(graph_id=graph_id, status="success", user_id=user_id)
|
||||
record_graph_operation(operation="execute", status="success")
|
||||
await increment_runs(user_id)
|
||||
await complete_re_run_agent(user_id, graph_id)
|
||||
if source == "library":
|
||||
await complete_onboarding_step(
|
||||
user_id, OnboardingStep.MARKETPLACE_RUN_AGENT
|
||||
)
|
||||
elif source == "builder":
|
||||
await complete_onboarding_step(user_id, OnboardingStep.BUILDER_RUN_AGENT)
|
||||
return result
|
||||
except GraphValidationError as e:
|
||||
# Record failed graph execution
|
||||
@@ -1103,6 +1141,15 @@ async def list_graph_executions(
|
||||
filtered_executions = await hide_activity_summaries_if_disabled(
|
||||
paginated_result.executions, user_id
|
||||
)
|
||||
onboarding = await get_user_onboarding(user_id)
|
||||
if (
|
||||
onboarding.onboardingAgentExecutionId
|
||||
and onboarding.onboardingAgentExecutionId
|
||||
in [exec.id for exec in filtered_executions]
|
||||
and OnboardingStep.GET_RESULTS not in onboarding.completedSteps
|
||||
):
|
||||
await complete_onboarding_step(user_id, OnboardingStep.GET_RESULTS)
|
||||
|
||||
return execution_db.GraphExecutionsPaginated(
|
||||
executions=filtered_executions, pagination=paginated_result.pagination
|
||||
)
|
||||
@@ -1140,6 +1187,12 @@ async def get_graph_execution(
|
||||
|
||||
# Apply feature flags to filter out disabled features
|
||||
result = await hide_activity_summary_if_disabled(result, user_id)
|
||||
onboarding = await get_user_onboarding(user_id)
|
||||
if (
|
||||
onboarding.onboardingAgentExecutionId == graph_exec_id
|
||||
and OnboardingStep.GET_RESULTS not in onboarding.completedSteps
|
||||
):
|
||||
await complete_onboarding_step(user_id, OnboardingStep.GET_RESULTS)
|
||||
|
||||
return result
|
||||
|
||||
@@ -1316,6 +1369,8 @@ async def create_graph_execution_schedule(
|
||||
result.next_run_time, user_timezone
|
||||
)
|
||||
|
||||
await complete_onboarding_step(user_id, OnboardingStep.SCHEDULE_AGENT)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
from typing import Literal, Optional
|
||||
|
||||
import autogpt_libs.auth as autogpt_auth_lib
|
||||
from fastapi import APIRouter, Body, HTTPException, Query, Security, status
|
||||
from fastapi.responses import Response
|
||||
from prisma.enums import OnboardingStep
|
||||
|
||||
import backend.server.v2.library.db as library_db
|
||||
import backend.server.v2.library.model as library_model
|
||||
import backend.server.v2.store.exceptions as store_exceptions
|
||||
from backend.data.onboarding import complete_onboarding_step
|
||||
from backend.util.exceptions import DatabaseError, NotFoundError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -200,6 +202,9 @@ async def get_library_agent_by_store_listing_version_id(
|
||||
)
|
||||
async def add_marketplace_agent_to_library(
|
||||
store_listing_version_id: str = Body(embed=True),
|
||||
source: Literal["onboarding", "marketplace"] = Body(
|
||||
default="marketplace", embed=True
|
||||
),
|
||||
user_id: str = Security(autogpt_auth_lib.get_user_id),
|
||||
) -> library_model.LibraryAgent:
|
||||
"""
|
||||
@@ -217,10 +222,15 @@ async def add_marketplace_agent_to_library(
|
||||
HTTPException(500): If a server/database error occurs.
|
||||
"""
|
||||
try:
|
||||
return await library_db.add_store_agent_to_library(
|
||||
agent = await library_db.add_store_agent_to_library(
|
||||
store_listing_version_id=store_listing_version_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
if source != "onboarding":
|
||||
await complete_onboarding_step(
|
||||
user_id, OnboardingStep.MARKETPLACE_ADD_AGENT
|
||||
)
|
||||
return agent
|
||||
|
||||
except store_exceptions.AgentNotFoundError as e:
|
||||
logger.warning(
|
||||
|
||||
@@ -10,6 +10,7 @@ from backend.data.execution import GraphExecutionMeta
|
||||
from backend.data.graph import get_graph
|
||||
from backend.data.integrations import get_webhook
|
||||
from backend.data.model import CredentialsMetaInput
|
||||
from backend.data.onboarding import increment_runs
|
||||
from backend.executor.utils import add_graph_execution, make_node_credentials_input_map
|
||||
from backend.integrations.creds_manager import IntegrationCredentialsManager
|
||||
from backend.integrations.webhooks import get_webhook_manager
|
||||
@@ -401,6 +402,8 @@ async def execute_preset(
|
||||
merged_node_input = preset.inputs | inputs
|
||||
merged_credential_inputs = preset.credentials | credential_inputs
|
||||
|
||||
await increment_runs(user_id)
|
||||
|
||||
return await add_graph_execution(
|
||||
user_id=user_id,
|
||||
graph_id=preset.graph_id,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import datetime
|
||||
import json
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import fastapi.testclient
|
||||
import pytest
|
||||
@@ -225,6 +226,10 @@ def test_add_agent_to_library_success(
|
||||
"backend.server.v2.library.db.add_store_agent_to_library"
|
||||
)
|
||||
mock_db_call.return_value = mock_library_agent
|
||||
mock_complete_onboarding = mocker.patch(
|
||||
"backend.server.v2.library.routes.agents.complete_onboarding_step",
|
||||
new_callable=AsyncMock,
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/agents", json={"store_listing_version_id": "test-version-id"}
|
||||
@@ -239,6 +244,7 @@ def test_add_agent_to_library_success(
|
||||
mock_db_call.assert_called_once_with(
|
||||
store_listing_version_id="test-version-id", user_id=test_user_id
|
||||
)
|
||||
mock_complete_onboarding.assert_awaited_once()
|
||||
|
||||
|
||||
def test_add_agent_to_library_error(mocker: pytest_mock.MockFixture, test_user_id: str):
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
"use client";
|
||||
import { StoreAgentDetails } from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { isEmptyOrWhitespace } from "@/lib/utils";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -13,15 +11,17 @@ import {
|
||||
OnboardingStep,
|
||||
} from "../components/OnboardingStep";
|
||||
import { OnboardingText } from "../components/OnboardingText";
|
||||
import { getV1RecommendedOnboardingAgents } from "@/app/api/__generated__/endpoints/onboarding/onboarding";
|
||||
import { resolveResponse } from "@/app/api/helpers";
|
||||
import { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
|
||||
|
||||
export default function Page() {
|
||||
const { state, updateState, completeStep } = useOnboarding(4, "INTEGRATIONS");
|
||||
const [agents, setAgents] = useState<StoreAgentDetails[]>([]);
|
||||
const api = useBackendAPI();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
api.getOnboardingAgents().then((agents) => {
|
||||
resolveResponse(getV1RecommendedOnboardingAgents()).then((agents) => {
|
||||
if (agents.length < 2) {
|
||||
completeStep("CONGRATS");
|
||||
router.replace("/");
|
||||
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
useGetV2GetAgentByVersion,
|
||||
useGetV2GetAgentGraph,
|
||||
} from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { resolveResponse } from "@/app/api/helpers";
|
||||
import { postV2AddMarketplaceAgent } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { GraphID } from "@/lib/autogpt-server-api";
|
||||
|
||||
export function useOnboardingRunStep() {
|
||||
const onboarding = useOnboarding(undefined, "AGENT_CHOICE");
|
||||
@@ -77,12 +80,7 @@ export function useOnboardingRunStep() {
|
||||
|
||||
setShowInput(true);
|
||||
onboarding.setStep(6);
|
||||
onboarding.updateState({
|
||||
completedSteps: [
|
||||
...(onboarding.state.completedSteps || []),
|
||||
"AGENT_NEW_RUN",
|
||||
],
|
||||
});
|
||||
onboarding.completeStep("AGENT_NEW_RUN");
|
||||
}
|
||||
|
||||
function handleSetAgentInput(key: string, value: string) {
|
||||
@@ -111,21 +109,22 @@ export function useOnboardingRunStep() {
|
||||
setRunningAgent(true);
|
||||
|
||||
try {
|
||||
const libraryAgent = await api.addMarketplaceAgentToLibrary(
|
||||
storeAgent?.store_listing_version_id || "",
|
||||
const libraryAgent = await resolveResponse(
|
||||
postV2AddMarketplaceAgent({
|
||||
store_listing_version_id: storeAgent?.store_listing_version_id || "",
|
||||
source: "onboarding",
|
||||
}),
|
||||
);
|
||||
|
||||
const { id: runID } = await api.executeGraph(
|
||||
libraryAgent.graph_id,
|
||||
libraryAgent.graph_id as GraphID,
|
||||
libraryAgent.graph_version,
|
||||
onboarding.state.agentInput || {},
|
||||
inputCredentials,
|
||||
"onboarding",
|
||||
);
|
||||
|
||||
onboarding.updateState({
|
||||
onboardingAgentExecutionId: runID,
|
||||
agentRuns: (onboarding.state.agentRuns || 0) + 1,
|
||||
});
|
||||
onboarding.updateState({ onboardingAgentExecutionId: runID });
|
||||
|
||||
router.push("/onboarding/6-congrats");
|
||||
} catch (error) {
|
||||
|
||||
@@ -5,6 +5,9 @@ import { useRouter } from "next/navigation";
|
||||
import * as party from "party-js";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider";
|
||||
import { resolveResponse } from "@/app/api/helpers";
|
||||
import { getV1OnboardingState } from "@/app/api/__generated__/endpoints/onboarding/onboarding";
|
||||
import { postV2AddMarketplaceAgent } from "@/app/api/__generated__/endpoints/library/library";
|
||||
|
||||
export default function Page() {
|
||||
const { completeStep } = useOnboarding(7, "AGENT_INPUT");
|
||||
@@ -37,11 +40,15 @@ export default function Page() {
|
||||
completeStep("CONGRATS");
|
||||
|
||||
try {
|
||||
const onboarding = await api.getUserOnboarding();
|
||||
const onboarding = await resolveResponse(getV1OnboardingState());
|
||||
if (onboarding?.selectedStoreListingVersionId) {
|
||||
try {
|
||||
const libraryAgent = await api.addMarketplaceAgentToLibrary(
|
||||
onboarding.selectedStoreListingVersionId,
|
||||
const libraryAgent = await resolveResponse(
|
||||
postV2AddMarketplaceAgent({
|
||||
store_listing_version_id:
|
||||
onboarding.selectedStoreListingVersionId,
|
||||
source: "onboarding",
|
||||
}),
|
||||
);
|
||||
router.replace(`/library/agents/${libraryAgent.id}`);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import StarRating from "./StarRating";
|
||||
import { StoreAgentDetails } from "@/lib/autogpt-server-api";
|
||||
import SmartImage from "@/components/__legacy__/SmartImage";
|
||||
import { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
|
||||
|
||||
type OnboardingAgentCardProps = {
|
||||
agent?: StoreAgentDetails;
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
"use client";
|
||||
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { resolveResponse, shouldShowOnboarding } from "@/app/api/helpers";
|
||||
import { getV1OnboardingState } from "@/app/api/__generated__/endpoints/onboarding/onboarding";
|
||||
|
||||
export default function OnboardingPage() {
|
||||
const router = useRouter();
|
||||
const api = useBackendAPI();
|
||||
|
||||
useEffect(() => {
|
||||
async function redirectToStep() {
|
||||
try {
|
||||
// Check if onboarding is enabled
|
||||
const isEnabled = await api.isOnboardingEnabled();
|
||||
const isEnabled = await shouldShowOnboarding();
|
||||
if (!isEnabled) {
|
||||
router.replace("/");
|
||||
return;
|
||||
}
|
||||
|
||||
const onboarding = await api.getUserOnboarding();
|
||||
const onboarding = await resolveResponse(getV1OnboardingState());
|
||||
|
||||
// Handle completed onboarding
|
||||
if (onboarding.completedSteps.includes("GET_RESULTS")) {
|
||||
@@ -66,7 +66,7 @@ export default function OnboardingPage() {
|
||||
}
|
||||
|
||||
redirectToStep();
|
||||
}, [api, router]);
|
||||
}, [router]);
|
||||
|
||||
return <LoadingSpinner size="large" cover />;
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ export const useRunGraph = () => {
|
||||
await executeGraph({
|
||||
graphId: flowID ?? "",
|
||||
graphVersion: flowVersion || null,
|
||||
data: { inputs: {}, credentials_inputs: {} },
|
||||
data: { inputs: {}, credentials_inputs: {}, source: "builder" },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -79,7 +79,11 @@ export const useRunInputDialog = ({
|
||||
await executeGraph({
|
||||
graphId: flowID ?? "",
|
||||
graphVersion: flowVersion || null,
|
||||
data: { inputs: inputValues, credentials_inputs: credentialValues },
|
||||
data: {
|
||||
inputs: inputValues,
|
||||
credentials_inputs: credentialValues,
|
||||
source: "builder",
|
||||
},
|
||||
});
|
||||
// Optimistically set running state immediately for responsive UI
|
||||
setIsGraphRunning(true);
|
||||
|
||||
@@ -83,7 +83,6 @@ export function RunnerInputDialog({
|
||||
onRun={doRun ? undefined : doClose}
|
||||
doCreateSchedule={doCreateSchedule ? handleSchedule : undefined}
|
||||
onCreateSchedule={doCreateSchedule ? undefined : doClose}
|
||||
runCount={0}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -152,7 +152,9 @@ export const useSaveGraph = ({
|
||||
links: graphLinks,
|
||||
};
|
||||
|
||||
const response = await createNewGraph({ data: { graph: data } });
|
||||
const response = await createNewGraph({
|
||||
data: { graph: data, source: "builder" },
|
||||
});
|
||||
const graphData = response.data as GraphModel;
|
||||
setGraphSchemas(
|
||||
graphData.input_schema,
|
||||
|
||||
@@ -16,7 +16,6 @@ import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutio
|
||||
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
|
||||
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
|
||||
import { useGetV1GetUserTimezone } from "@/app/api/__generated__/endpoints/auth/auth";
|
||||
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
|
||||
import { analytics } from "@/services/analytics";
|
||||
|
||||
export type RunVariant =
|
||||
@@ -50,7 +49,6 @@ export function useAgentRunModal(
|
||||
const [cronExpression, setCronExpression] = useState(
|
||||
agent.recommended_schedule_cron || "0 9 * * 1",
|
||||
);
|
||||
const { completeStep: completeOnboardingStep } = useOnboarding();
|
||||
|
||||
// Get user timezone for scheduling
|
||||
const { data: userTimezone } = useGetV1GetUserTimezone({
|
||||
@@ -290,6 +288,7 @@ export function useAgentRunModal(
|
||||
data: {
|
||||
inputs: inputValues,
|
||||
credentials_inputs: inputCredentials,
|
||||
source: "library",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -335,8 +334,6 @@ export function useAgentRunModal(
|
||||
userTimezone && userTimezone !== "not-set" ? userTimezone : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
completeOnboardingStep("SCHEDULE_AGENT");
|
||||
}, [
|
||||
allRequiredInputsAreSet,
|
||||
scheduleName,
|
||||
|
||||
@@ -77,6 +77,7 @@ export function useSelectedRunActions(args: Args) {
|
||||
data: {
|
||||
inputs: args.run.inputs || {},
|
||||
credentials_inputs: args.run.credential_inputs || {},
|
||||
source: "library",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ import { CreatePresetDialog } from "./components/create-preset-dialog";
|
||||
import { useAgentRunsInfinite } from "./use-agent-runs";
|
||||
import { AgentRunsSelectorList } from "./components/agent-runs-selector-list";
|
||||
import { AgentScheduleDetailsView } from "./components/agent-schedule-details-view";
|
||||
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
|
||||
|
||||
export function OldAgentLibraryView() {
|
||||
const { id: agentID }: { id: LibraryAgentID } = useParams();
|
||||
@@ -84,11 +83,6 @@ export function OldAgentLibraryView() {
|
||||
useState<GraphExecutionMeta | null>(null);
|
||||
const [confirmingDeleteAgentPreset, setConfirmingDeleteAgentPreset] =
|
||||
useState<LibraryAgentPresetID | null>(null);
|
||||
const {
|
||||
state: onboardingState,
|
||||
updateState: updateOnboardingState,
|
||||
incrementRuns,
|
||||
} = useOnboarding();
|
||||
const [copyAgentDialogOpen, setCopyAgentDialogOpen] = useState(false);
|
||||
const [creatingPresetFromExecutionID, setCreatingPresetFromExecutionID] =
|
||||
useState<GraphExecutionID | null>(null);
|
||||
@@ -136,22 +130,6 @@ export function OldAgentLibraryView() {
|
||||
[api, graphVersions, loadingGraphVersions],
|
||||
);
|
||||
|
||||
// Reward user for viewing results of their onboarding agent
|
||||
useEffect(() => {
|
||||
if (
|
||||
!onboardingState ||
|
||||
!selectedRun ||
|
||||
onboardingState.completedSteps.includes("GET_RESULTS")
|
||||
)
|
||||
return;
|
||||
|
||||
if (selectedRun.id === onboardingState.onboardingAgentExecutionId) {
|
||||
updateOnboardingState({
|
||||
completedSteps: [...onboardingState.completedSteps, "GET_RESULTS"],
|
||||
});
|
||||
}
|
||||
}, [selectedRun, onboardingState, updateOnboardingState]);
|
||||
|
||||
const lastRefresh = useRef<number>(0);
|
||||
const refreshPageData = useCallback(() => {
|
||||
if (Date.now() - lastRefresh.current < 2e3) return; // 2 second debounce
|
||||
@@ -285,10 +263,6 @@ export function OldAgentLibraryView() {
|
||||
(data) => {
|
||||
if (data.graph_id != agent?.graph_id) return;
|
||||
|
||||
if (data.status == "COMPLETED") {
|
||||
incrementRuns();
|
||||
}
|
||||
|
||||
agentRunsQuery.upsertAgentRun(data);
|
||||
if (data.id === selectedView.id) {
|
||||
// Update currently viewed run
|
||||
@@ -300,7 +274,7 @@ export function OldAgentLibraryView() {
|
||||
return () => {
|
||||
detachExecUpdateHandler();
|
||||
};
|
||||
}, [api, agent?.graph_id, selectedView.id, incrementRuns]);
|
||||
}, [api, agent?.graph_id, selectedView.id]);
|
||||
|
||||
// Pre-load selectedRun based on selectedView
|
||||
useEffect(() => {
|
||||
@@ -558,7 +532,6 @@ export function OldAgentLibraryView() {
|
||||
onCreateSchedule={onCreateSchedule}
|
||||
onCreatePreset={onCreatePreset}
|
||||
agentActions={agentActions}
|
||||
runCount={agentRuns.length}
|
||||
recommendedScheduleCron={agent?.recommended_schedule_cron || null}
|
||||
/>
|
||||
) : selectedView.type == "preset" ? (
|
||||
@@ -574,7 +547,6 @@ export function OldAgentLibraryView() {
|
||||
onUpdatePreset={onUpdatePreset}
|
||||
doDeletePreset={setConfirmingDeleteAgentPreset}
|
||||
agentActions={agentActions}
|
||||
runCount={agentRuns.length}
|
||||
/>
|
||||
) : selectedView.type == "schedule" ? (
|
||||
selectedSchedule &&
|
||||
|
||||
@@ -38,7 +38,6 @@ import { AgentRunStatus, agentRunStatusMap } from "./agent-run-status-chip";
|
||||
import useCredits from "@/hooks/useCredits";
|
||||
import { AgentRunOutputView } from "./agent-run-output-view";
|
||||
import { analytics } from "@/services/analytics";
|
||||
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
|
||||
import { PendingReviewsList } from "@/components/organisms/PendingReviewsList/PendingReviewsList";
|
||||
import { usePendingReviewsForExecution } from "@/hooks/usePendingReviews";
|
||||
|
||||
@@ -67,8 +66,6 @@ export function AgentRunDetailsView({
|
||||
[run],
|
||||
);
|
||||
|
||||
const { completeStep } = useOnboarding();
|
||||
|
||||
const {
|
||||
pendingReviews,
|
||||
isLoading: reviewsLoading,
|
||||
@@ -166,13 +163,13 @@ export function AgentRunDetailsView({
|
||||
graph.version,
|
||||
run.inputs!,
|
||||
run.credential_inputs!,
|
||||
"library",
|
||||
)
|
||||
.then(({ id }) => {
|
||||
analytics.sendDatafastEvent("run_agent", {
|
||||
name: graph.name,
|
||||
id: graph.id,
|
||||
});
|
||||
completeStep("RE_RUN_AGENT");
|
||||
onRun(id);
|
||||
})
|
||||
.catch(toastOnFail("execute agent"));
|
||||
|
||||
@@ -40,9 +40,8 @@ import { cn, isEmpty } from "@/lib/utils";
|
||||
import { ClockIcon, CopyIcon, InfoIcon } from "@phosphor-icons/react";
|
||||
import { CalendarClockIcon, Trash2Icon } from "lucide-react";
|
||||
|
||||
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
|
||||
import { analytics } from "@/services/analytics";
|
||||
import { AgentStatus, AgentStatusChip } from "./agent-status-chip";
|
||||
import { analytics } from "@/services/analytics";
|
||||
|
||||
export function AgentRunDraftView({
|
||||
graph,
|
||||
@@ -55,7 +54,6 @@ export function AgentRunDraftView({
|
||||
doCreateSchedule: _doCreateSchedule,
|
||||
onCreateSchedule,
|
||||
agentActions,
|
||||
runCount,
|
||||
className,
|
||||
recommendedScheduleCron,
|
||||
}: {
|
||||
@@ -74,7 +72,6 @@ export function AgentRunDraftView({
|
||||
credentialsInputs: Record<string, CredentialsMetaInput>,
|
||||
) => Promise<void>;
|
||||
onCreateSchedule?: (schedule: Schedule) => void;
|
||||
runCount: number;
|
||||
className?: string;
|
||||
} & (
|
||||
| {
|
||||
@@ -103,7 +100,6 @@ export function AgentRunDraftView({
|
||||
const [changedPresetAttributes, setChangedPresetAttributes] = useState<
|
||||
Set<keyof LibraryAgentPresetUpdatable>
|
||||
>(new Set());
|
||||
const { completeStep: completeOnboardingStep } = useOnboarding();
|
||||
const [cronScheduleDialogOpen, setCronScheduleDialogOpen] = useState(false);
|
||||
|
||||
// Update values if agentPreset parameter is changed
|
||||
@@ -193,7 +189,13 @@ export function AgentRunDraftView({
|
||||
}
|
||||
// TODO: on executing preset with changes, ask for confirmation and offer save+run
|
||||
const newRun = await api
|
||||
.executeGraph(graph.id, graph.version, inputValues, inputCredentials)
|
||||
.executeGraph(
|
||||
graph.id,
|
||||
graph.version,
|
||||
inputValues,
|
||||
inputCredentials,
|
||||
"library",
|
||||
)
|
||||
.catch(toastOnFail("execute agent"));
|
||||
|
||||
if (newRun && onRun) onRun(newRun.id);
|
||||
@@ -203,26 +205,12 @@ export function AgentRunDraftView({
|
||||
.then((newRun) => onRun && onRun(newRun.id))
|
||||
.catch(toastOnFail("execute agent preset"));
|
||||
}
|
||||
// Mark run agent onboarding step as completed
|
||||
completeOnboardingStep("MARKETPLACE_RUN_AGENT");
|
||||
|
||||
analytics.sendDatafastEvent("run_agent", {
|
||||
name: graph.name,
|
||||
id: graph.id,
|
||||
});
|
||||
|
||||
if (runCount > 0) {
|
||||
completeOnboardingStep("RE_RUN_AGENT");
|
||||
}
|
||||
}, [
|
||||
api,
|
||||
graph,
|
||||
inputValues,
|
||||
inputCredentials,
|
||||
onRun,
|
||||
toastOnFail,
|
||||
completeOnboardingStep,
|
||||
]);
|
||||
}, [api, graph, inputValues, inputCredentials, onRun, toastOnFail]);
|
||||
|
||||
const doCreatePreset = useCallback(async () => {
|
||||
if (!onCreatePreset) return;
|
||||
@@ -256,7 +244,6 @@ export function AgentRunDraftView({
|
||||
onCreatePreset,
|
||||
toast,
|
||||
toastOnFail,
|
||||
completeOnboardingStep,
|
||||
]);
|
||||
|
||||
const doUpdatePreset = useCallback(async () => {
|
||||
@@ -295,7 +282,6 @@ export function AgentRunDraftView({
|
||||
onUpdatePreset,
|
||||
toast,
|
||||
toastOnFail,
|
||||
completeOnboardingStep,
|
||||
]);
|
||||
|
||||
const doSetPresetActive = useCallback(
|
||||
@@ -342,7 +328,6 @@ export function AgentRunDraftView({
|
||||
onCreatePreset,
|
||||
toast,
|
||||
toastOnFail,
|
||||
completeOnboardingStep,
|
||||
]);
|
||||
|
||||
const openScheduleDialog = useCallback(() => {
|
||||
|
||||
@@ -100,6 +100,7 @@ export function AgentScheduleDetailsView({
|
||||
graph.version,
|
||||
schedule.input_data,
|
||||
schedule.input_credentials,
|
||||
"library",
|
||||
)
|
||||
.then((run) => onForcedRun(run.id))
|
||||
.catch(toastOnFail("execute agent")),
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import { useGetV1GetUserTimezone } from "@/app/api/__generated__/endpoints/auth/auth";
|
||||
import { getTimezoneDisplayName } from "@/lib/timezone-utils";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
|
||||
|
||||
// Base type for cron expression only
|
||||
type CronOnlyCallback = (cronExpression: string) => void;
|
||||
@@ -49,7 +48,6 @@ export function CronSchedulerDialog(props: CronSchedulerDialogProps) {
|
||||
const [scheduleName, setScheduleName] = useState<string>(
|
||||
props.mode === "with-name" ? props.defaultScheduleName || "" : "",
|
||||
);
|
||||
const { completeStep } = useOnboarding();
|
||||
|
||||
// Get user's timezone
|
||||
const { data: userTimezone } = useGetV1GetUserTimezone({
|
||||
@@ -94,7 +92,6 @@ export function CronSchedulerDialog(props: CronSchedulerDialogProps) {
|
||||
props.onSubmit(cronExpression);
|
||||
}
|
||||
setOpen(false);
|
||||
completeStep("SCHEDULE_AGENT");
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -62,6 +62,7 @@ export const useLibraryUploadAgentDialog = () => {
|
||||
await createGraph({
|
||||
data: {
|
||||
graph: payload,
|
||||
source: "upload",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { useGetV2DownloadAgentFile } from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
|
||||
import { analytics } from "@/services/analytics";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
@@ -18,7 +17,6 @@ interface UseAgentInfoProps {
|
||||
export const useAgentInfo = ({ storeListingVersionId }: UseAgentInfoProps) => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { completeStep } = useOnboarding();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
@@ -49,8 +47,6 @@ export const useAgentInfo = ({ storeListingVersionId }: UseAgentInfoProps) => {
|
||||
const data = response as LibraryAgent;
|
||||
|
||||
if (isAddingAgentFirstTime) {
|
||||
completeStep("MARKETPLACE_ADD_AGENT");
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListLibraryAgentsQueryKey(),
|
||||
});
|
||||
|
||||
@@ -62,7 +62,7 @@ export const AgentImportForm: React.FC<
|
||||
};
|
||||
|
||||
api
|
||||
.createGraph(payload)
|
||||
.createGraph(payload, "upload")
|
||||
.then((response) => {
|
||||
const qID = "flowID";
|
||||
window.location.href = `/build?${qID}=${response.id}`;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
import {
|
||||
getV1IsOnboardingEnabled,
|
||||
getV1OnboardingState,
|
||||
} from "./__generated__/endpoints/onboarding/onboarding";
|
||||
|
||||
/**
|
||||
* Narrow an orval response to its success payload if and only if it is a `200` status with OK shape.
|
||||
@@ -26,10 +29,67 @@ export function okData<T>(res: unknown): T | undefined {
|
||||
return (res as { data: T }).data;
|
||||
}
|
||||
|
||||
type ResponseWithData = { status: number; data: unknown };
|
||||
type ExtractResponseData<T extends ResponseWithData> = T extends {
|
||||
data: infer D;
|
||||
}
|
||||
? D
|
||||
: never;
|
||||
type SuccessfulResponses<T extends ResponseWithData> = T extends {
|
||||
status: infer S;
|
||||
}
|
||||
? S extends number
|
||||
? `${S}` extends `2${string}`
|
||||
? T
|
||||
: never
|
||||
: never
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Resolve an Orval response to its payload after asserting the status is either the explicit
|
||||
* `expected` code or any other 2xx status if `expected` is omitted.
|
||||
*
|
||||
* Usage with server actions:
|
||||
* ```ts
|
||||
* const onboarding = await expectStatus(getV1OnboardingState());
|
||||
* const agent = await expectStatus(
|
||||
* postV2AddMarketplaceAgent({ store_listing_version_id }),
|
||||
* 201,
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export function resolveResponse<
|
||||
TSuccess extends ResponseWithData,
|
||||
TCode extends number,
|
||||
>(
|
||||
promise: Promise<TSuccess>,
|
||||
expected: TCode,
|
||||
): Promise<ExtractResponseData<Extract<TSuccess, { status: TCode }>>>;
|
||||
export function resolveResponse<TSuccess extends ResponseWithData>(
|
||||
promise: Promise<TSuccess>,
|
||||
): Promise<ExtractResponseData<SuccessfulResponses<TSuccess>>>;
|
||||
export async function resolveResponse<
|
||||
TSuccess extends ResponseWithData,
|
||||
TCode extends number,
|
||||
>(promise: Promise<TSuccess>, expected?: TCode) {
|
||||
const res = await promise;
|
||||
const isSuccessfulStatus =
|
||||
typeof res.status === "number" && res.status >= 200 && res.status < 300;
|
||||
|
||||
if (typeof expected === "number") {
|
||||
if (res.status !== expected) {
|
||||
throw new Error(`Unexpected status ${res.status}`);
|
||||
}
|
||||
} else if (!isSuccessfulStatus) {
|
||||
throw new Error(`Unexpected status ${res.status}`);
|
||||
}
|
||||
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function shouldShowOnboarding() {
|
||||
const api = new BackendAPI();
|
||||
const isEnabled = await api.isOnboardingEnabled();
|
||||
const onboarding = await api.getUserOnboarding();
|
||||
const isEnabled = await resolveResponse(getV1IsOnboardingEnabled());
|
||||
const onboarding = await resolveResponse(getV1OnboardingState());
|
||||
const isCompleted = onboarding.completedSteps.includes("CONGRATS");
|
||||
return isEnabled && !isCompleted;
|
||||
}
|
||||
|
||||
@@ -827,12 +827,16 @@
|
||||
"/api/onboarding": {
|
||||
"get": {
|
||||
"tags": ["v1", "onboarding"],
|
||||
"summary": "Get onboarding status",
|
||||
"operationId": "getV1Get onboarding status",
|
||||
"summary": "Onboarding state",
|
||||
"operationId": "getV1Onboarding state",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": { "application/json": { "schema": {} } }
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/UserOnboarding" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
|
||||
@@ -842,8 +846,8 @@
|
||||
},
|
||||
"patch": {
|
||||
"tags": ["v1", "onboarding"],
|
||||
"summary": "Update onboarding progress",
|
||||
"operationId": "patchV1Update onboarding progress",
|
||||
"summary": "Update onboarding state",
|
||||
"operationId": "patchV1Update onboarding state",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
@@ -855,7 +859,11 @@
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": { "application/json": { "schema": {} } }
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/UserOnboarding" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
@@ -872,16 +880,71 @@
|
||||
"security": [{ "HTTPBearerJWT": [] }]
|
||||
}
|
||||
},
|
||||
"/api/onboarding/agents": {
|
||||
"get": {
|
||||
"/api/onboarding/step": {
|
||||
"post": {
|
||||
"tags": ["v1", "onboarding"],
|
||||
"summary": "Get recommended agents",
|
||||
"operationId": "getV1Get recommended agents",
|
||||
"summary": "Complete onboarding step",
|
||||
"operationId": "postV1Complete onboarding step",
|
||||
"security": [{ "HTTPBearerJWT": [] }],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "step",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"enum": [
|
||||
"WELCOME",
|
||||
"USAGE_REASON",
|
||||
"INTEGRATIONS",
|
||||
"AGENT_CHOICE",
|
||||
"AGENT_NEW_RUN",
|
||||
"AGENT_INPUT",
|
||||
"CONGRATS",
|
||||
"MARKETPLACE_VISIT",
|
||||
"BUILDER_OPEN"
|
||||
],
|
||||
"type": "string",
|
||||
"title": "Step"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": { "application/json": { "schema": {} } }
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/onboarding/agents": {
|
||||
"get": {
|
||||
"tags": ["v1", "onboarding"],
|
||||
"summary": "Recommended onboarding agents",
|
||||
"operationId": "getV1Recommended onboarding agents",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": { "$ref": "#/components/schemas/StoreAgentDetails" },
|
||||
"type": "array",
|
||||
"title": "Response Getv1Recommended Onboarding Agents"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
|
||||
}
|
||||
@@ -892,12 +955,19 @@
|
||||
"/api/onboarding/enabled": {
|
||||
"get": {
|
||||
"tags": ["v1", "onboarding", "public"],
|
||||
"summary": "Check onboarding enabled",
|
||||
"operationId": "getV1Check onboarding enabled",
|
||||
"summary": "Is onboarding enabled",
|
||||
"operationId": "getV1Is onboarding enabled",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": { "application/json": { "schema": {} } }
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"title": "Response Getv1Is Onboarding Enabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
|
||||
@@ -914,7 +984,11 @@
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": { "application/json": { "schema": {} } }
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/UserOnboarding" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
|
||||
@@ -5665,6 +5739,16 @@
|
||||
},
|
||||
"type": "object",
|
||||
"title": "Credentials Inputs"
|
||||
},
|
||||
"source": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["builder", "library", "onboarding"]
|
||||
},
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Source"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -5712,6 +5796,12 @@
|
||||
"store_listing_version_id": {
|
||||
"type": "string",
|
||||
"title": "Store Listing Version Id"
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"enum": ["onboarding", "marketplace"],
|
||||
"title": "Source",
|
||||
"default": "marketplace"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -5819,7 +5909,16 @@
|
||||
"title": "CreateAPIKeyResponse"
|
||||
},
|
||||
"CreateGraph": {
|
||||
"properties": { "graph": { "$ref": "#/components/schemas/Graph" } },
|
||||
"properties": {
|
||||
"graph": { "$ref": "#/components/schemas/Graph" },
|
||||
"source": {
|
||||
"anyOf": [
|
||||
{ "type": "string", "enum": ["builder", "upload"] },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Source"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["graph"],
|
||||
"title": "CreateGraph"
|
||||
@@ -10289,18 +10388,87 @@
|
||||
"title": "UserHistoryResponse",
|
||||
"description": "Response model for listings with version history"
|
||||
},
|
||||
"UserOnboardingUpdate": {
|
||||
"UserOnboarding": {
|
||||
"properties": {
|
||||
"userId": { "type": "string", "title": "Userid" },
|
||||
"completedSteps": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": { "$ref": "#/components/schemas/OnboardingStep" },
|
||||
"type": "array"
|
||||
},
|
||||
{ "type": "null" }
|
||||
],
|
||||
"items": { "$ref": "#/components/schemas/OnboardingStep" },
|
||||
"type": "array",
|
||||
"title": "Completedsteps"
|
||||
},
|
||||
"walletShown": { "type": "boolean", "title": "Walletshown" },
|
||||
"notified": {
|
||||
"items": { "$ref": "#/components/schemas/OnboardingStep" },
|
||||
"type": "array",
|
||||
"title": "Notified"
|
||||
},
|
||||
"rewardedFor": {
|
||||
"items": { "$ref": "#/components/schemas/OnboardingStep" },
|
||||
"type": "array",
|
||||
"title": "Rewardedfor"
|
||||
},
|
||||
"usageReason": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Usagereason"
|
||||
},
|
||||
"integrations": {
|
||||
"items": { "type": "string" },
|
||||
"type": "array",
|
||||
"title": "Integrations"
|
||||
},
|
||||
"otherIntegrations": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Otherintegrations"
|
||||
},
|
||||
"selectedStoreListingVersionId": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Selectedstorelistingversionid"
|
||||
},
|
||||
"agentInput": {
|
||||
"anyOf": [
|
||||
{ "additionalProperties": true, "type": "object" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Agentinput"
|
||||
},
|
||||
"onboardingAgentExecutionId": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Onboardingagentexecutionid"
|
||||
},
|
||||
"agentRuns": { "type": "integer", "title": "Agentruns" },
|
||||
"lastRunAt": {
|
||||
"anyOf": [
|
||||
{ "type": "string", "format": "date-time" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Lastrunat"
|
||||
},
|
||||
"consecutiveRunDays": {
|
||||
"type": "integer",
|
||||
"title": "Consecutiverundays"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"userId",
|
||||
"completedSteps",
|
||||
"walletShown",
|
||||
"notified",
|
||||
"rewardedFor",
|
||||
"usageReason",
|
||||
"integrations",
|
||||
"otherIntegrations",
|
||||
"selectedStoreListingVersionId",
|
||||
"agentInput",
|
||||
"onboardingAgentExecutionId",
|
||||
"agentRuns",
|
||||
"lastRunAt",
|
||||
"consecutiveRunDays"
|
||||
],
|
||||
"title": "UserOnboarding"
|
||||
},
|
||||
"UserOnboardingUpdate": {
|
||||
"properties": {
|
||||
"walletShown": {
|
||||
"anyOf": [{ "type": "boolean" }, { "type": "null" }],
|
||||
"title": "Walletshown"
|
||||
@@ -10344,21 +10512,6 @@
|
||||
"onboardingAgentExecutionId": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Onboardingagentexecutionid"
|
||||
},
|
||||
"agentRuns": {
|
||||
"anyOf": [{ "type": "integer" }, { "type": "null" }],
|
||||
"title": "Agentruns"
|
||||
},
|
||||
"lastRunAt": {
|
||||
"anyOf": [
|
||||
{ "type": "string", "format": "date-time" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Lastrunat"
|
||||
},
|
||||
"consecutiveRunDays": {
|
||||
"anyOf": [{ "type": "integer" }, { "type": "null" }],
|
||||
"title": "Consecutiverundays"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
|
||||
@@ -250,31 +250,41 @@ export function Wallet() {
|
||||
[],
|
||||
);
|
||||
|
||||
// Confetti effect on the wallet button
|
||||
// React to onboarding notifications emitted by the provider
|
||||
const handleNotification = useCallback(
|
||||
(notification: WebSocketNotification) => {
|
||||
if (notification.type !== "onboarding") {
|
||||
if (
|
||||
notification.type !== "onboarding" ||
|
||||
notification.event !== "step_completed" ||
|
||||
!walletRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (walletRef.current) {
|
||||
// Fix confetti appearing in the top left corner
|
||||
const rect = walletRef.current.getBoundingClientRect();
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
return;
|
||||
}
|
||||
fetchCredits();
|
||||
party.confetti(walletRef.current!, {
|
||||
count: 30,
|
||||
spread: 120,
|
||||
shapes: ["square", "circle"],
|
||||
size: party.variation.range(1, 2),
|
||||
speed: party.variation.range(200, 300),
|
||||
modules: [fadeOut],
|
||||
});
|
||||
// Only trigger confetti for tasks that are in groups
|
||||
const taskIds = groups
|
||||
.flatMap((group) => group.tasks)
|
||||
.map((task) => task.id);
|
||||
if (!taskIds.includes(notification.step as OnboardingStep)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = walletRef.current.getBoundingClientRect();
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchCredits();
|
||||
party.confetti(walletRef.current, {
|
||||
count: 30,
|
||||
spread: 120,
|
||||
shapes: ["square", "circle"],
|
||||
size: party.variation.range(1, 2),
|
||||
speed: party.variation.range(200, 300),
|
||||
modules: [fadeOut],
|
||||
});
|
||||
},
|
||||
[],
|
||||
[fetchCredits, fadeOut],
|
||||
);
|
||||
|
||||
// WebSocket setup for onboarding notifications
|
||||
|
||||
@@ -16,7 +16,10 @@ export function TaskGroups({ groups }: Props) {
|
||||
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>(() => {
|
||||
const initialState: Record<string, boolean> = {};
|
||||
groups.forEach((group) => {
|
||||
initialState[group.name] = true;
|
||||
const completed = group.tasks.every((task) =>
|
||||
state?.completedSteps?.includes(task.id),
|
||||
);
|
||||
initialState[group.name] = !completed;
|
||||
});
|
||||
return initialState;
|
||||
});
|
||||
@@ -62,7 +65,7 @@ export function TaskGroups({ groups }: Props) {
|
||||
{} as Record<string, boolean>,
|
||||
),
|
||||
);
|
||||
}, [state?.completedSteps, isGroupCompleted]);
|
||||
}, [state?.completedSteps, isGroupCompleted, groups]);
|
||||
|
||||
const setRef = (name: string) => (el: HTMLDivElement | null) => {
|
||||
if (el) {
|
||||
@@ -101,9 +104,10 @@ export function TaskGroups({ groups }: Props) {
|
||||
useEffect(() => {
|
||||
groups.forEach((group) => {
|
||||
const groupCompleted = isGroupCompleted(group);
|
||||
// Check if the last task in the group is completed
|
||||
const alreadyCelebrated = state?.notified.includes(
|
||||
group.tasks[group.tasks.length - 1].id,
|
||||
// Check if all tasks in the group were already celebrated
|
||||
// last task completed triggers group completion
|
||||
const alreadyCelebrated = group.tasks.every((task) =>
|
||||
state?.notified.includes(task.id),
|
||||
);
|
||||
|
||||
if (groupCompleted) {
|
||||
|
||||
@@ -26,7 +26,6 @@ import { default as NextLink } from "next/link";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
|
||||
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { getGetV2ListLibraryAgentsQueryKey } from "@/app/api/__generated__/endpoints/library/library";
|
||||
|
||||
@@ -67,7 +66,6 @@ export default function useAgentGraph(
|
||||
>(null);
|
||||
const [xyNodes, setXYNodes] = useState<CustomNode[]>([]);
|
||||
const [xyEdges, setXYEdges] = useState<CustomEdge[]>([]);
|
||||
const { state, completeStep, incrementRuns } = useOnboarding();
|
||||
const betaBlocks = useGetFlag(Flag.BETA_BLOCKS);
|
||||
|
||||
// Filter blocks based on beta flags
|
||||
@@ -563,14 +561,13 @@ export default function useAgentGraph(
|
||||
setIsRunning(false);
|
||||
setIsStopping(false);
|
||||
setActiveExecutionID(null);
|
||||
incrementRuns();
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
fetchExecutions();
|
||||
}, [flowID, flowExecutionID, incrementRuns]);
|
||||
}, [flowID, flowExecutionID]);
|
||||
|
||||
const prepareNodeInputData = useCallback(
|
||||
(node: CustomNode) => {
|
||||
@@ -679,7 +676,7 @@ export default function useAgentGraph(
|
||||
...payload,
|
||||
id: savedAgent.id,
|
||||
})
|
||||
: await api.createGraph(payload);
|
||||
: await api.createGraph(payload, "builder");
|
||||
|
||||
console.debug("Response from the API:", newSavedAgent);
|
||||
}
|
||||
@@ -751,8 +748,6 @@ export default function useAgentGraph(
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListLibraryAgentsQueryKey(),
|
||||
});
|
||||
|
||||
completeStep("BUILDER_SAVE_AGENT");
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
@@ -765,7 +760,7 @@ export default function useAgentGraph(
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [_saveAgent, toast, completeStep]);
|
||||
}, [_saveAgent, toast]);
|
||||
|
||||
const saveAndRun = useCallback(
|
||||
async (
|
||||
@@ -780,7 +775,6 @@ export default function useAgentGraph(
|
||||
let savedAgent: Graph;
|
||||
try {
|
||||
savedAgent = await _saveAgent();
|
||||
completeStep("BUILDER_SAVE_AGENT");
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
@@ -808,6 +802,7 @@ export default function useAgentGraph(
|
||||
savedAgent.version,
|
||||
inputs,
|
||||
credentialsInputs,
|
||||
"builder",
|
||||
);
|
||||
|
||||
setActiveExecutionID(graphExecution.id);
|
||||
@@ -818,10 +813,6 @@ export default function useAgentGraph(
|
||||
path.set("flowVersion", savedAgent.version.toString());
|
||||
path.set("flowExecutionID", graphExecution.id);
|
||||
router.push(`${pathname}?${path.toString()}`);
|
||||
|
||||
if (state?.completedSteps.includes("BUILDER_SAVE_AGENT")) {
|
||||
completeStep("BUILDER_RUN_AGENT");
|
||||
}
|
||||
} catch (error) {
|
||||
// Check if this is a structured validation error from the backend
|
||||
if (error instanceof ApiError && error.isGraphValidationError()) {
|
||||
@@ -871,12 +862,10 @@ export default function useAgentGraph(
|
||||
[
|
||||
_saveAgent,
|
||||
toast,
|
||||
completeStep,
|
||||
api,
|
||||
searchParams,
|
||||
pathname,
|
||||
router,
|
||||
state,
|
||||
isSaving,
|
||||
isRunning,
|
||||
processedUpdates,
|
||||
|
||||
@@ -55,7 +55,6 @@ import type {
|
||||
Schedule,
|
||||
ScheduleCreatable,
|
||||
ScheduleID,
|
||||
StoreAgentDetails,
|
||||
StoreAgentsResponse,
|
||||
StoreListingsWithVersionsResponse,
|
||||
StoreReview,
|
||||
@@ -66,7 +65,6 @@ import type {
|
||||
SubmissionStatus,
|
||||
TransactionHistory,
|
||||
User,
|
||||
UserOnboarding,
|
||||
UserPasswordCredentials,
|
||||
UsersBalanceHistoryResponse,
|
||||
WebSocketNotification,
|
||||
@@ -193,29 +191,6 @@ export default class BackendAPI {
|
||||
return this._request("PATCH", "/credits");
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
////////////// ONBOARDING //////////////
|
||||
////////////////////////////////////////
|
||||
|
||||
getUserOnboarding(): Promise<UserOnboarding> {
|
||||
return this._get("/onboarding");
|
||||
}
|
||||
|
||||
updateUserOnboarding(
|
||||
onboarding: Omit<Partial<UserOnboarding>, "rewardedFor">,
|
||||
): Promise<void> {
|
||||
return this._request("PATCH", "/onboarding", onboarding);
|
||||
}
|
||||
|
||||
getOnboardingAgents(): Promise<StoreAgentDetails[]> {
|
||||
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 ////////////////
|
||||
////////////////////////////////////////
|
||||
@@ -249,8 +224,14 @@ export default class BackendAPI {
|
||||
return this._get(`/graphs/${id}/versions`);
|
||||
}
|
||||
|
||||
createGraph(graph: GraphCreatable): Promise<Graph> {
|
||||
const requestBody = { graph } as GraphCreateRequestBody;
|
||||
createGraph(
|
||||
graph: GraphCreatable,
|
||||
source?: GraphCreationSource,
|
||||
): Promise<Graph> {
|
||||
const requestBody: GraphCreateRequestBody = { graph };
|
||||
if (source) {
|
||||
requestBody.source = source;
|
||||
}
|
||||
|
||||
return this._request("POST", "/graphs", requestBody);
|
||||
}
|
||||
@@ -274,11 +255,13 @@ export default class BackendAPI {
|
||||
version: number,
|
||||
inputs: { [key: string]: any } = {},
|
||||
credentials_inputs: { [key: string]: CredentialsMetaInput } = {},
|
||||
source?: GraphExecutionSource,
|
||||
): Promise<GraphExecutionMeta> {
|
||||
return this._request("POST", `/graphs/${id}/execute/${version}`, {
|
||||
inputs,
|
||||
credentials_inputs,
|
||||
});
|
||||
const body: GraphExecuteRequestBody = { inputs, credentials_inputs };
|
||||
if (source) {
|
||||
body.source = source;
|
||||
}
|
||||
return this._request("POST", `/graphs/${id}/execute/${version}`, body);
|
||||
}
|
||||
|
||||
getExecutions(): Promise<GraphExecutionMeta[]> {
|
||||
@@ -468,29 +451,12 @@ export default class BackendAPI {
|
||||
return this._get("/store/agents", params);
|
||||
}
|
||||
|
||||
getStoreAgent(
|
||||
username: string,
|
||||
agentName: string,
|
||||
): Promise<StoreAgentDetails> {
|
||||
return this._get(
|
||||
`/store/agents/${encodeURIComponent(username)}/${encodeURIComponent(
|
||||
agentName,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
getGraphMetaByStoreListingVersionID(
|
||||
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;
|
||||
@@ -689,14 +655,6 @@ export default class BackendAPI {
|
||||
});
|
||||
}
|
||||
|
||||
addMarketplaceAgentToLibrary(
|
||||
storeListingVersionID: string,
|
||||
): Promise<LibraryAgent> {
|
||||
return this._request("POST", "/library/agents", {
|
||||
store_listing_version_id: storeListingVersionID,
|
||||
});
|
||||
}
|
||||
|
||||
updateLibraryAgent(
|
||||
libraryAgentId: LibraryAgentID,
|
||||
params: {
|
||||
@@ -1356,8 +1314,18 @@ declare global {
|
||||
|
||||
/* *** UTILITY TYPES *** */
|
||||
|
||||
type GraphCreationSource = "builder" | "upload";
|
||||
type GraphExecutionSource = "builder" | "library" | "onboarding";
|
||||
|
||||
type GraphCreateRequestBody = {
|
||||
graph: GraphCreatable;
|
||||
source?: GraphCreationSource;
|
||||
};
|
||||
|
||||
type GraphExecuteRequestBody = {
|
||||
inputs: { [key: string]: any };
|
||||
credentials_inputs: { [key: string]: CredentialsMetaInput };
|
||||
source?: GraphExecutionSource;
|
||||
};
|
||||
|
||||
type WebsocketMessageTypeMap = {
|
||||
|
||||
@@ -761,28 +761,6 @@ export type StoreAgentsResponse = {
|
||||
pagination: Pagination;
|
||||
};
|
||||
|
||||
export type StoreAgentDetails = {
|
||||
store_listing_version_id: string;
|
||||
slug: string;
|
||||
updated_at: string;
|
||||
agent_name: string;
|
||||
agent_video: string;
|
||||
agent_image: string[];
|
||||
creator: string;
|
||||
creator_avatar: string;
|
||||
sub_heading: string;
|
||||
description: string;
|
||||
categories: string[];
|
||||
runs: number;
|
||||
rating: number;
|
||||
versions: string[];
|
||||
|
||||
// Approval and status fields
|
||||
active_version_id?: string;
|
||||
has_approved_version?: boolean;
|
||||
is_available?: boolean;
|
||||
};
|
||||
|
||||
export type Creator = {
|
||||
name: string;
|
||||
username: string;
|
||||
@@ -1028,8 +1006,8 @@ export interface UserOnboarding {
|
||||
|
||||
export interface OnboardingNotificationPayload {
|
||||
type: "onboarding";
|
||||
event: string;
|
||||
step: OnboardingStep;
|
||||
event: "step_completed" | "increment_runs";
|
||||
step: OnboardingStep | null;
|
||||
}
|
||||
|
||||
export type WebSocketNotification =
|
||||
|
||||
@@ -1,78 +1,32 @@
|
||||
import { OnboardingStep, UserOnboarding } from "@/lib/autogpt-server-api";
|
||||
import {
|
||||
GraphExecutionID,
|
||||
OnboardingStep,
|
||||
UserOnboarding,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { UserOnboarding as RawUserOnboarding } from "@/app/api/__generated__/models/userOnboarding";
|
||||
|
||||
export function isToday(date: Date): boolean {
|
||||
const today = new Date();
|
||||
return (
|
||||
date.getDate() === today.getDate() &&
|
||||
date.getMonth() === today.getMonth() &&
|
||||
date.getFullYear() === today.getFullYear()
|
||||
);
|
||||
}
|
||||
export type LocalOnboardingStateUpdate = Omit<
|
||||
Partial<UserOnboarding>,
|
||||
| "completedSteps"
|
||||
| "rewardedFor"
|
||||
| "lastRunAt"
|
||||
| "consecutiveRunDays"
|
||||
| "agentRuns"
|
||||
>;
|
||||
|
||||
export function isYesterday(date: Date): boolean {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
return (
|
||||
date.getDate() === yesterday.getDate() &&
|
||||
date.getMonth() === yesterday.getMonth() &&
|
||||
date.getFullYear() === yesterday.getFullYear()
|
||||
);
|
||||
}
|
||||
|
||||
export function calculateConsecutiveDays(
|
||||
lastRunAt: Date | null,
|
||||
currentConsecutiveDays: number,
|
||||
): { lastRunAt: Date; consecutiveRunDays: number } {
|
||||
const now = new Date();
|
||||
|
||||
if (lastRunAt === null || isYesterday(lastRunAt)) {
|
||||
return {
|
||||
lastRunAt: now,
|
||||
consecutiveRunDays: currentConsecutiveDays + 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (!isToday(lastRunAt)) {
|
||||
return {
|
||||
lastRunAt: now,
|
||||
consecutiveRunDays: 1,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
lastRunAt: now,
|
||||
consecutiveRunDays: currentConsecutiveDays,
|
||||
};
|
||||
}
|
||||
|
||||
export function getRunMilestoneSteps(
|
||||
newRunCount: number,
|
||||
consecutiveDays: number,
|
||||
): OnboardingStep[] {
|
||||
const steps: OnboardingStep[] = [];
|
||||
|
||||
if (newRunCount >= 10) steps.push("RUN_AGENTS");
|
||||
if (newRunCount >= 100) steps.push("RUN_AGENTS_100");
|
||||
if (consecutiveDays >= 3) steps.push("RUN_3_DAYS");
|
||||
if (consecutiveDays >= 14) steps.push("RUN_14_DAYS");
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
export function processOnboardingData(
|
||||
onboarding: UserOnboarding,
|
||||
export function fromBackendUserOnboarding(
|
||||
onboarding: RawUserOnboarding,
|
||||
): UserOnboarding {
|
||||
// Patch for TRIGGER_WEBHOOK - only set on backend then overwritten by frontend
|
||||
const completeWebhook =
|
||||
onboarding.rewardedFor.includes("TRIGGER_WEBHOOK") &&
|
||||
!onboarding.completedSteps.includes("TRIGGER_WEBHOOK")
|
||||
? (["TRIGGER_WEBHOOK"] as OnboardingStep[])
|
||||
: [];
|
||||
|
||||
return {
|
||||
...onboarding,
|
||||
completedSteps: [...completeWebhook, ...onboarding.completedSteps],
|
||||
usageReason: onboarding.usageReason || null,
|
||||
otherIntegrations: onboarding.otherIntegrations || null,
|
||||
selectedStoreListingVersionId:
|
||||
onboarding.selectedStoreListingVersionId || null,
|
||||
agentInput:
|
||||
(onboarding.agentInput as Record<string, string | number>) || null,
|
||||
onboardingAgentExecutionId:
|
||||
(onboarding.onboardingAgentExecutionId as GraphExecutionID) || null,
|
||||
lastRunAt: onboarding.lastRunAt ? new Date(onboarding.lastRunAt) : null,
|
||||
};
|
||||
}
|
||||
@@ -87,23 +41,30 @@ export function shouldRedirectFromOnboarding(
|
||||
);
|
||||
}
|
||||
|
||||
export function createInitialOnboardingState(
|
||||
newState: Omit<Partial<UserOnboarding>, "rewardedFor">,
|
||||
): UserOnboarding {
|
||||
export function updateOnboardingState(
|
||||
prevState: UserOnboarding | null,
|
||||
newState: LocalOnboardingStateUpdate,
|
||||
): UserOnboarding | null {
|
||||
return {
|
||||
completedSteps: [],
|
||||
walletShown: true,
|
||||
notified: [],
|
||||
rewardedFor: [],
|
||||
usageReason: null,
|
||||
integrations: [],
|
||||
otherIntegrations: null,
|
||||
selectedStoreListingVersionId: null,
|
||||
agentInput: null,
|
||||
onboardingAgentExecutionId: null,
|
||||
agentRuns: 0,
|
||||
lastRunAt: null,
|
||||
consecutiveRunDays: 0,
|
||||
...newState,
|
||||
completedSteps: prevState?.completedSteps ?? [],
|
||||
walletShown: newState.walletShown ?? prevState?.walletShown ?? false,
|
||||
notified: newState.notified ?? prevState?.notified ?? [],
|
||||
rewardedFor: prevState?.rewardedFor ?? [],
|
||||
usageReason: newState.usageReason ?? prevState?.usageReason ?? null,
|
||||
integrations: newState.integrations ?? prevState?.integrations ?? [],
|
||||
otherIntegrations:
|
||||
newState.otherIntegrations ?? prevState?.otherIntegrations ?? null,
|
||||
selectedStoreListingVersionId:
|
||||
newState.selectedStoreListingVersionId ??
|
||||
prevState?.selectedStoreListingVersionId ??
|
||||
null,
|
||||
agentInput: newState.agentInput ?? prevState?.agentInput ?? null,
|
||||
onboardingAgentExecutionId:
|
||||
newState.onboardingAgentExecutionId ??
|
||||
prevState?.onboardingAgentExecutionId ??
|
||||
null,
|
||||
lastRunAt: prevState?.lastRunAt ?? null,
|
||||
consecutiveRunDays: prevState?.consecutiveRunDays ?? 0,
|
||||
agentRuns: prevState?.agentRuns ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,7 +10,10 @@ import {
|
||||
} from "@/components/__legacy__/ui/dialog";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useOnboardingTimezoneDetection } from "@/hooks/useOnboardingTimezoneDetection";
|
||||
import { OnboardingStep, UserOnboarding } from "@/lib/autogpt-server-api";
|
||||
import {
|
||||
UserOnboarding,
|
||||
WebSocketNotification,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import Link from "next/link";
|
||||
@@ -25,28 +28,37 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
calculateConsecutiveDays,
|
||||
createInitialOnboardingState,
|
||||
getRunMilestoneSteps,
|
||||
processOnboardingData,
|
||||
updateOnboardingState,
|
||||
fromBackendUserOnboarding,
|
||||
shouldRedirectFromOnboarding,
|
||||
LocalOnboardingStateUpdate,
|
||||
} from "./helpers";
|
||||
import { resolveResponse } from "@/app/api/helpers";
|
||||
import {
|
||||
getV1IsOnboardingEnabled,
|
||||
getV1OnboardingState,
|
||||
patchV1UpdateOnboardingState,
|
||||
postV1CompleteOnboardingStep,
|
||||
} from "@/app/api/__generated__/endpoints/onboarding/onboarding";
|
||||
import { PostV1CompleteOnboardingStepStep } from "@/app/api/__generated__/models/postV1CompleteOnboardingStepStep";
|
||||
|
||||
type FrontendOnboardingStep = PostV1CompleteOnboardingStepStep;
|
||||
|
||||
const OnboardingContext = createContext<
|
||||
| {
|
||||
state: UserOnboarding | null;
|
||||
updateState: (
|
||||
state: Omit<Partial<UserOnboarding>, "rewardedFor">,
|
||||
) => void;
|
||||
updateState: (state: LocalOnboardingStateUpdate) => void;
|
||||
step: number;
|
||||
setStep: (step: number) => void;
|
||||
completeStep: (step: OnboardingStep) => void;
|
||||
incrementRuns: () => void;
|
||||
completeStep: (step: FrontendOnboardingStep) => void;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
export function useOnboarding(step?: number, completeStep?: OnboardingStep) {
|
||||
export function useOnboarding(
|
||||
step?: number,
|
||||
completeStep?: FrontendOnboardingStep,
|
||||
) {
|
||||
const context = useContext(OnboardingContext);
|
||||
|
||||
if (!context)
|
||||
@@ -56,15 +68,13 @@ export function useOnboarding(step?: number, completeStep?: OnboardingStep) {
|
||||
if (
|
||||
!completeStep ||
|
||||
!context.state ||
|
||||
!context.state.completedSteps ||
|
||||
context.state.completedSteps.includes(completeStep)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.updateState({
|
||||
completedSteps: [...context.state.completedSteps, completeStep],
|
||||
});
|
||||
}, [completeStep, context, context.updateState]);
|
||||
context.completeStep(completeStep);
|
||||
}, [completeStep, context]);
|
||||
|
||||
useEffect(() => {
|
||||
if (step && context.step !== step) {
|
||||
@@ -113,6 +123,15 @@ export default function OnboardingProvider({
|
||||
|
||||
const isOnOnboardingRoute = pathname.startsWith("/onboarding");
|
||||
|
||||
const fetchOnboarding = useCallback(async () => {
|
||||
const onboarding = await resolveResponse(getV1OnboardingState());
|
||||
const processedOnboarding = fromBackendUserOnboarding(onboarding);
|
||||
if (isMounted.current) {
|
||||
setState(processedOnboarding);
|
||||
}
|
||||
return processedOnboarding;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Prevent multiple initializations
|
||||
if (hasInitialized.current || !isLoggedIn) {
|
||||
@@ -125,26 +144,19 @@ export default function OnboardingProvider({
|
||||
try {
|
||||
// Check onboarding enabled only for onboarding routes
|
||||
if (isOnOnboardingRoute) {
|
||||
const enabled = await api.isOnboardingEnabled();
|
||||
const enabled = await resolveResponse(getV1IsOnboardingEnabled());
|
||||
if (!enabled) {
|
||||
router.push("/marketplace");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const onboarding = await api.getUserOnboarding();
|
||||
if (!onboarding) return;
|
||||
|
||||
const processedOnboarding = processOnboardingData(onboarding);
|
||||
setState(processedOnboarding);
|
||||
const onboarding = await fetchOnboarding();
|
||||
|
||||
// Handle redirects for completed onboarding
|
||||
if (
|
||||
isOnOnboardingRoute &&
|
||||
shouldRedirectFromOnboarding(
|
||||
processedOnboarding.completedSteps,
|
||||
pathname,
|
||||
)
|
||||
shouldRedirectFromOnboarding(onboarding.completedSteps, pathname)
|
||||
) {
|
||||
router.push("/marketplace");
|
||||
}
|
||||
@@ -163,21 +175,53 @@ export default function OnboardingProvider({
|
||||
initializeOnboarding();
|
||||
}, [api, isOnOnboardingRoute, router, isLoggedIn, pathname]);
|
||||
|
||||
const updateState = useCallback(
|
||||
(newState: Omit<Partial<UserOnboarding>, "rewardedFor">) => {
|
||||
if (!isLoggedIn || !isMounted.current) return;
|
||||
const handleOnboardingNotification = useCallback(
|
||||
(notification: WebSocketNotification) => {
|
||||
if (!isLoggedIn || notification.type !== "onboarding") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update local state immediately
|
||||
setState((prev) => {
|
||||
if (!prev) {
|
||||
return createInitialOnboardingState(newState);
|
||||
}
|
||||
return { ...prev, ...newState };
|
||||
if (notification.step === "RUN_AGENTS") {
|
||||
setNpsDialogOpen(true);
|
||||
}
|
||||
|
||||
fetchOnboarding().catch((error) => {
|
||||
console.error(
|
||||
"Failed to refresh onboarding after notification:",
|
||||
error,
|
||||
);
|
||||
});
|
||||
},
|
||||
[fetchOnboarding, isLoggedIn],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const detachMessage = api.onWebSocketMessage(
|
||||
"notification",
|
||||
handleOnboardingNotification,
|
||||
);
|
||||
|
||||
if (isLoggedIn) {
|
||||
api.connectWebSocket();
|
||||
}
|
||||
|
||||
return () => {
|
||||
detachMessage();
|
||||
};
|
||||
}, [api, handleOnboardingNotification, isLoggedIn]);
|
||||
|
||||
const updateState = useCallback(
|
||||
(newState: LocalOnboardingStateUpdate) => {
|
||||
if (!isLoggedIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState((prev) => updateOnboardingState(prev, newState));
|
||||
|
||||
const updatePromise = (async () => {
|
||||
try {
|
||||
await api.updateUserOnboarding(newState);
|
||||
if (!isMounted.current) return;
|
||||
await patchV1UpdateOnboardingState(newState);
|
||||
} catch (error) {
|
||||
console.error("Failed to update user onboarding:", error);
|
||||
|
||||
@@ -188,58 +232,54 @@ export default function OnboardingProvider({
|
||||
}
|
||||
})();
|
||||
|
||||
// Track this pending update
|
||||
pendingUpdatesRef.current.add(updatePromise);
|
||||
|
||||
updatePromise.finally(() => {
|
||||
pendingUpdatesRef.current.delete(updatePromise);
|
||||
});
|
||||
},
|
||||
[api, isLoggedIn, isMounted],
|
||||
[toast, isLoggedIn, fetchOnboarding, api, setState],
|
||||
);
|
||||
|
||||
const completeStep = useCallback(
|
||||
(step: OnboardingStep) => {
|
||||
if (!state?.completedSteps?.includes(step)) {
|
||||
updateState({
|
||||
completedSteps: [...(state?.completedSteps || []), step],
|
||||
});
|
||||
(step: FrontendOnboardingStep) => {
|
||||
if (!isLoggedIn || state?.completedSteps?.includes(step)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const completionPromise = (async () => {
|
||||
try {
|
||||
await postV1CompleteOnboardingStep({ step });
|
||||
await fetchOnboarding();
|
||||
} catch (error) {
|
||||
if (isMounted.current) {
|
||||
console.error("Failed to complete onboarding step:", error);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Failed to complete onboarding step",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
pendingUpdatesRef.current.add(completionPromise);
|
||||
completionPromise.finally(() => {
|
||||
pendingUpdatesRef.current.delete(completionPromise);
|
||||
});
|
||||
},
|
||||
[state?.completedSteps, updateState],
|
||||
[isLoggedIn, state?.completedSteps, fetchOnboarding, toast],
|
||||
);
|
||||
|
||||
const incrementRuns = useCallback(() => {
|
||||
if (!state?.completedSteps) return;
|
||||
|
||||
const newRunCount = state.agentRuns + 1;
|
||||
const consecutiveData = calculateConsecutiveDays(
|
||||
state.lastRunAt,
|
||||
state.consecutiveRunDays,
|
||||
);
|
||||
|
||||
const milestoneSteps = getRunMilestoneSteps(
|
||||
newRunCount,
|
||||
consecutiveData.consecutiveRunDays,
|
||||
);
|
||||
|
||||
// Show NPS dialog at 10 runs
|
||||
if (newRunCount === 10) {
|
||||
setNpsDialogOpen(true);
|
||||
}
|
||||
|
||||
updateState({
|
||||
agentRuns: newRunCount,
|
||||
completedSteps: Array.from(
|
||||
new Set([...state.completedSteps, ...milestoneSteps]),
|
||||
),
|
||||
...consecutiveData,
|
||||
});
|
||||
}, [state, updateState]);
|
||||
|
||||
return (
|
||||
<OnboardingContext.Provider
|
||||
value={{ state, updateState, step, setStep, completeStep, incrementRuns }}
|
||||
value={{
|
||||
state,
|
||||
updateState,
|
||||
step,
|
||||
setStep,
|
||||
completeStep,
|
||||
}}
|
||||
>
|
||||
<Dialog onOpenChange={setNpsDialogOpen} open={npsDialogOpen}>
|
||||
<DialogContent>
|
||||
|
||||
Reference in New Issue
Block a user