Skip to content

Commit d791cde

Browse files
authored
feat(platform): Onboarding Phase 2 (#9736)
### Changes 🏗️ - Update onboarding to give user rewards for completing steps - Remove `canvas-confetti` lib and add `party-js` instead; the former didn't allow to play confetti from a component - Add onboarding videos in `frontend/public/onboarding/` - Remove Balance (`CreditsCard.tsx`) and add openable `Wallet.tsx` (and accompanying `WalletTaskGroup.tsx`) instead that displays grouped onboarding tasks with descriptions and short instructional videos - Further relevant updates to `useOnboarding`, `types.ts` - Implement onboarding rewards - Add `onboarding_reward` function in `credit.py` that is used to reward user for finished onboarding tasks safely - transaction key is deterministic, so the same user won't be rewarded twice for the same step. - Add `reward_user` in `onboarding.py` - Update `UserOnboarding` model and add a migration <img width="464" alt="Screenshot 2025-04-05 at 6 06 29 PM" src="https://github.com/user-attachments/assets/fca8d09e-0139-466b-b679-d24117ad01f0" /> ### 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 works - [x] Tasks can be completed - [x] Rewards are added correctly for all completed tasks
1 parent bb92226 commit d791cde

File tree

31 files changed

+698
-131
lines changed

31 files changed

+698
-131
lines changed

autogpt_platform/backend/backend/data/credit.py

+34
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
CreditRefundRequestStatus,
1212
CreditTransactionType,
1313
NotificationType,
14+
OnboardingStep,
1415
)
1516
from prisma.errors import UniqueViolationError
1617
from prisma.models import CreditRefundRequest, CreditTransaction, User
@@ -121,6 +122,18 @@ async def top_up_credits(self, user_id: str, amount: int):
121122
"""
122123
pass
123124

125+
@abstractmethod
126+
async def onboarding_reward(self, user_id: str, credits: int, step: OnboardingStep):
127+
"""
128+
Reward the user with credits for completing an onboarding step.
129+
Won't reward if the user has already received credits for the step.
130+
131+
Args:
132+
user_id (str): The user ID.
133+
step (OnboardingStep): The onboarding step.
134+
"""
135+
pass
136+
124137
@abstractmethod
125138
async def top_up_intent(self, user_id: str, amount: int) -> str:
126139
"""
@@ -408,6 +421,24 @@ async def spend_credits(
408421
async def top_up_credits(self, user_id: str, amount: int):
409422
await self._top_up_credits(user_id, amount)
410423

424+
async def onboarding_reward(self, user_id: str, credits: int, step: OnboardingStep):
425+
key = f"REWARD-{user_id}-{step.value}"
426+
if not await CreditTransaction.prisma().find_first(
427+
where={
428+
"userId": user_id,
429+
"transactionKey": key,
430+
}
431+
):
432+
await self._add_transaction(
433+
user_id=user_id,
434+
amount=credits,
435+
transaction_type=CreditTransactionType.GRANT,
436+
transaction_key=key,
437+
metadata=Json(
438+
{"reason": f"Reward for completing {step.value} onboarding step."}
439+
),
440+
)
441+
411442
async def top_up_refund(
412443
self, user_id: str, transaction_key: str, metadata: dict[str, str]
413444
) -> int:
@@ -895,6 +926,9 @@ async def spend_credits(self, *args, **kwargs) -> int:
895926
async def top_up_credits(self, *args, **kwargs):
896927
pass
897928

929+
async def onboarding_reward(self, *args, **kwargs):
930+
pass
931+
898932
async def top_up_intent(self, *args, **kwargs) -> str:
899933
return ""
900934

autogpt_platform/backend/backend/data/onboarding.py

+62
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
from prisma.models import UserOnboarding
99
from prisma.types import UserOnboardingCreateInput, UserOnboardingUpdateInput
1010

11+
from backend.data import db
1112
from backend.data.block import get_blocks
13+
from backend.data.credit import get_user_credit_model
1214
from backend.data.graph import GraphModel
1315
from backend.data.model import CredentialsMetaInput
1416
from backend.server.v2.store.model import StoreAgentDetails
@@ -24,14 +26,19 @@
2426
POINTS_AGENT_COUNT = 50 # Number of agents to calculate points for
2527
MIN_AGENT_COUNT = 2 # Minimum number of marketplace agents to enable onboarding
2628

29+
user_credit = get_user_credit_model()
30+
2731

2832
class UserOnboardingUpdate(pydantic.BaseModel):
2933
completedSteps: Optional[list[OnboardingStep]] = None
34+
notificationDot: Optional[bool] = None
35+
notified: Optional[list[OnboardingStep]] = None
3036
usageReason: Optional[str] = None
3137
integrations: Optional[list[str]] = None
3238
otherIntegrations: Optional[str] = None
3339
selectedStoreListingVersionId: Optional[str] = None
3440
agentInput: Optional[dict[str, Any]] = None
41+
onboardingAgentExecutionId: Optional[str] = None
3542

3643

3744
async def get_user_onboarding(user_id: str):
@@ -48,6 +55,20 @@ async def update_user_onboarding(user_id: str, data: UserOnboardingUpdate):
4855
update: UserOnboardingUpdateInput = {}
4956
if data.completedSteps is not None:
5057
update["completedSteps"] = list(set(data.completedSteps))
58+
for step in (
59+
OnboardingStep.AGENT_NEW_RUN,
60+
OnboardingStep.GET_RESULTS,
61+
OnboardingStep.MARKETPLACE_ADD_AGENT,
62+
OnboardingStep.MARKETPLACE_RUN_AGENT,
63+
OnboardingStep.BUILDER_SAVE_AGENT,
64+
OnboardingStep.BUILDER_RUN_AGENT,
65+
):
66+
if step in data.completedSteps:
67+
await reward_user(user_id, step)
68+
if data.notificationDot is not None:
69+
update["notificationDot"] = data.notificationDot
70+
if data.notified is not None:
71+
update["notified"] = list(set(data.notified))
5172
if data.usageReason is not None:
5273
update["usageReason"] = data.usageReason
5374
if data.integrations is not None:
@@ -58,6 +79,8 @@ async def update_user_onboarding(user_id: str, data: UserOnboardingUpdate):
5879
update["selectedStoreListingVersionId"] = data.selectedStoreListingVersionId
5980
if data.agentInput is not None:
6081
update["agentInput"] = Json(data.agentInput)
82+
if data.onboardingAgentExecutionId is not None:
83+
update["onboardingAgentExecutionId"] = data.onboardingAgentExecutionId
6184

6285
return await UserOnboarding.prisma().upsert(
6386
where={"userId": user_id},
@@ -68,6 +91,45 @@ async def update_user_onboarding(user_id: str, data: UserOnboardingUpdate):
6891
)
6992

7093

94+
async def reward_user(user_id: str, step: OnboardingStep):
95+
async with db.locked_transaction(f"usr_trx_{user_id}-reward"):
96+
reward = 0
97+
match step:
98+
# Reward user when they clicked New Run during onboarding
99+
# This is because they need credits before scheduling a run (next step)
100+
case OnboardingStep.AGENT_NEW_RUN:
101+
reward = 300
102+
case OnboardingStep.GET_RESULTS:
103+
reward = 300
104+
case OnboardingStep.MARKETPLACE_ADD_AGENT:
105+
reward = 100
106+
case OnboardingStep.MARKETPLACE_RUN_AGENT:
107+
reward = 100
108+
case OnboardingStep.BUILDER_SAVE_AGENT:
109+
reward = 100
110+
case OnboardingStep.BUILDER_RUN_AGENT:
111+
reward = 100
112+
113+
if reward == 0:
114+
return
115+
116+
onboarding = await get_user_onboarding(user_id)
117+
118+
# Skip if already rewarded
119+
if step in onboarding.rewardedFor:
120+
return
121+
122+
onboarding.rewardedFor.append(step)
123+
await user_credit.onboarding_reward(user_id, reward, step)
124+
await UserOnboarding.prisma().update(
125+
where={"userId": user_id},
126+
data={
127+
"completedSteps": list(set(onboarding.completedSteps + [step])),
128+
"rewardedFor": onboarding.rewardedFor,
129+
},
130+
)
131+
132+
71133
def clean_and_split(text: str) -> list[str]:
72134
"""
73135
Removes all special characters from a string, truncates it to 100 characters,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-- Modify the OnboardingStep enum
2+
ALTER TYPE "OnboardingStep" ADD VALUE 'GET_RESULTS';
3+
ALTER TYPE "OnboardingStep" ADD VALUE 'MARKETPLACE_VISIT';
4+
ALTER TYPE "OnboardingStep" ADD VALUE 'MARKETPLACE_ADD_AGENT';
5+
ALTER TYPE "OnboardingStep" ADD VALUE 'MARKETPLACE_RUN_AGENT';
6+
ALTER TYPE "OnboardingStep" ADD VALUE 'BUILDER_OPEN';
7+
ALTER TYPE "OnboardingStep" ADD VALUE 'BUILDER_SAVE_AGENT';
8+
ALTER TYPE "OnboardingStep" ADD VALUE 'BUILDER_RUN_AGENT';
9+
10+
-- Modify the UserOnboarding table
11+
ALTER TABLE "UserOnboarding"
12+
ADD COLUMN "updatedAt" TIMESTAMP(3),
13+
ADD COLUMN "notificationDot" BOOLEAN NOT NULL DEFAULT true,
14+
ADD COLUMN "notified" "OnboardingStep"[] DEFAULT '{}',
15+
ADD COLUMN "rewardedFor" "OnboardingStep"[] DEFAULT '{}',
16+
ADD COLUMN "onboardingAgentExecutionId" TEXT

autogpt_platform/backend/schema.prisma

+15
Original file line numberDiff line numberDiff line change
@@ -58,25 +58,40 @@ model User {
5858
}
5959

6060
enum OnboardingStep {
61+
// Introductory onboarding (Library)
6162
WELCOME
6263
USAGE_REASON
6364
INTEGRATIONS
6465
AGENT_CHOICE
6566
AGENT_NEW_RUN
6667
AGENT_INPUT
6768
CONGRATS
69+
GET_RESULTS
70+
// Marketplace
71+
MARKETPLACE_VISIT
72+
MARKETPLACE_ADD_AGENT
73+
MARKETPLACE_RUN_AGENT
74+
// Builder
75+
BUILDER_OPEN
76+
BUILDER_SAVE_AGENT
77+
BUILDER_RUN_AGENT
6878
}
6979

7080
model UserOnboarding {
7181
id String @id @default(uuid())
7282
createdAt DateTime @default(now())
83+
updatedAt DateTime? @updatedAt
7384
7485
completedSteps OnboardingStep[] @default([])
86+
notificationDot Boolean @default(true)
87+
notified OnboardingStep[] @default([])
88+
rewardedFor OnboardingStep[] @default([])
7589
usageReason String?
7690
integrations String[] @default([])
7791
otherIntegrations String?
7892
selectedStoreListingVersionId String?
7993
agentInput Json?
94+
onboardingAgentExecutionId String?
8095
8196
userId String @unique
8297
User User @relation(fields: [userId], references: [id], onDelete: Cascade)

autogpt_platform/frontend/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@
5252
"@xyflow/react": "12.4.2",
5353
"ajv": "^8.17.1",
5454
"boring-avatars": "^1.11.2",
55-
"canvas-confetti": "^1.9.3",
5655
"class-variance-authority": "^0.7.1",
5756
"clsx": "^2.1.1",
5857
"cmdk": "1.0.4",
@@ -70,6 +69,7 @@
7069
"moment": "^2.30.1",
7170
"next": "^14.2.26",
7271
"next-themes": "^0.4.5",
72+
"party-js": "^2.2.0",
7373
"react": "^18",
7474
"react-day-picker": "^9.6.1",
7575
"react-dom": "^18",
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

autogpt_platform/frontend/src/app/build/page.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,16 @@
33
import { useSearchParams } from "next/navigation";
44
import { GraphID } from "@/lib/autogpt-server-api/types";
55
import FlowEditor from "@/components/Flow";
6+
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
7+
import { useEffect } from "react";
68

79
export default function Home() {
810
const query = useSearchParams();
11+
const { completeStep } = useOnboarding();
12+
13+
useEffect(() => {
14+
completeStep("BUILDER_OPEN");
15+
}, []);
916

1017
return (
1118
<FlowEditor

autogpt_platform/frontend/src/app/library/agents/[id]/page.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import AgentRunDraftView from "@/components/agents/agent-run-draft-view";
2222
import AgentRunDetailsView from "@/components/agents/agent-run-details-view";
2323
import AgentRunsSelectorList from "@/components/agents/agent-runs-selector-list";
2424
import AgentScheduleDetailsView from "@/components/agents/agent-schedule-details-view";
25+
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
2526

2627
export default function AgentRunsPage(): React.ReactElement {
2728
const { id: agentID }: { id: LibraryAgentID } = useParams();
@@ -49,6 +50,7 @@ export default function AgentRunsPage(): React.ReactElement {
4950
useState<boolean>(false);
5051
const [confirmingDeleteAgentRun, setConfirmingDeleteAgentRun] =
5152
useState<GraphExecutionMeta | null>(null);
53+
const { state, updateState } = useOnboarding();
5254

5355
const openRunDraftView = useCallback(() => {
5456
selectView({ type: "run" });
@@ -78,6 +80,18 @@ export default function AgentRunsPage(): React.ReactElement {
7880
[api, graphVersions],
7981
);
8082

83+
// Reward user for viewing results of their onboarding agent
84+
useEffect(() => {
85+
if (!state || !selectedRun || state.completedSteps.includes("GET_RESULTS"))
86+
return;
87+
88+
if (selectedRun.id === state.onboardingAgentExecutionId) {
89+
updateState({
90+
completedSteps: [...state.completedSteps, "GET_RESULTS"],
91+
});
92+
}
93+
}, [selectedRun, state]);
94+
8195
const fetchAgents = useCallback(() => {
8296
api.getLibraryAgent(agentID).then((agent) => {
8397
setAgent(agent);

autogpt_platform/frontend/src/app/onboarding/5-run/page.tsx

+8-2
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,14 @@ export default function Page() {
8383
api.addMarketplaceAgentToLibrary(
8484
storeAgent?.store_listing_version_id || "",
8585
);
86-
api.executeGraph(agent.id, agent.version, state?.agentInput || {});
87-
router.push("/onboarding/6-congrats");
86+
api
87+
.executeGraph(agent.id, agent.version, state?.agentInput || {})
88+
.then(({ graph_exec_id }) => {
89+
updateState({
90+
onboardingAgentExecutionId: graph_exec_id,
91+
});
92+
router.push("/onboarding/6-congrats");
93+
});
8894
}, [api, agent, router, state?.agentInput]);
8995

9096
const runYourAgent = (

autogpt_platform/frontend/src/app/onboarding/6-congrats/actions.ts

-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ import { redirect } from "next/navigation";
66
export async function finishOnboarding() {
77
const api = new BackendAPI();
88
const onboarding = await api.getUserOnboarding();
9-
await api.updateUserOnboarding({
10-
completedSteps: [...onboarding.completedSteps, "CONGRATS"],
11-
});
129
revalidatePath("/library", "layout");
1310
redirect("/library");
1411
}

autogpt_platform/frontend/src/app/onboarding/6-congrats/page.tsx

+18-12
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
"use client";
2-
import { useEffect, useState } from "react";
2+
import { useEffect, useRef, useState } from "react";
33
import { cn } from "@/lib/utils";
44
import { finishOnboarding } from "./actions";
5-
import confetti from "canvas-confetti";
65
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
6+
import * as party from "party-js";
77

88
export default function Page() {
9-
useOnboarding(7, "AGENT_INPUT");
9+
const { state, updateState } = useOnboarding(7, "AGENT_INPUT");
1010
const [showText, setShowText] = useState(false);
1111
const [showSubtext, setShowSubtext] = useState(false);
12+
const divRef = useRef(null);
1213

1314
useEffect(() => {
14-
confetti({
15-
particleCount: 120,
16-
spread: 360,
17-
shapes: ["square", "circle"],
18-
scalar: 2,
19-
decay: 0.93,
20-
origin: { y: 0.38, x: 0.51 },
21-
});
15+
if (divRef.current) {
16+
party.confetti(divRef.current, {
17+
count: 100,
18+
spread: 180,
19+
shapes: ["square", "circle"],
20+
size: party.variation.range(2, 2), // scalar: 2
21+
speed: party.variation.range(300, 1000),
22+
});
23+
}
2224

2325
const timer0 = setTimeout(() => {
2426
setShowText(true);
@@ -29,6 +31,9 @@ export default function Page() {
2931
}, 500);
3032

3133
const timer2 = setTimeout(() => {
34+
updateState({
35+
completedSteps: [...(state?.completedSteps || []), "CONGRATS"],
36+
});
3237
finishOnboarding();
3338
}, 3000);
3439

@@ -42,6 +47,7 @@ export default function Page() {
4247
return (
4348
<div className="flex h-screen w-screen flex-col items-center justify-center bg-violet-100">
4449
<div
50+
ref={divRef}
4551
className={cn(
4652
"z-10 -mb-16 text-9xl duration-500",
4753
showText ? "opacity-100" : "opacity-0",
@@ -63,7 +69,7 @@ export default function Page() {
6369
showSubtext ? "opacity-100" : "opacity-0",
6470
)}
6571
>
66-
You earned 15$ for running your first agent
72+
You earned 3$ for running your first agent
6773
</p>
6874
</div>
6975
);

0 commit comments

Comments
 (0)