mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 15:17:59 -05:00
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
This commit is contained in:
committed by
GitHub
parent
bb92226f5d
commit
d791cdea76
@@ -11,6 +11,7 @@ from prisma.enums import (
|
||||
CreditRefundRequestStatus,
|
||||
CreditTransactionType,
|
||||
NotificationType,
|
||||
OnboardingStep,
|
||||
)
|
||||
from prisma.errors import UniqueViolationError
|
||||
from prisma.models import CreditRefundRequest, CreditTransaction, User
|
||||
@@ -121,6 +122,18 @@ class UserCreditBase(ABC):
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def onboarding_reward(self, user_id: str, credits: int, step: OnboardingStep):
|
||||
"""
|
||||
Reward the user with credits for completing an onboarding step.
|
||||
Won't reward if the user has already received credits for the step.
|
||||
|
||||
Args:
|
||||
user_id (str): The user ID.
|
||||
step (OnboardingStep): The onboarding step.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def top_up_intent(self, user_id: str, amount: int) -> str:
|
||||
"""
|
||||
@@ -408,6 +421,24 @@ class UserCredit(UserCreditBase):
|
||||
async def top_up_credits(self, user_id: str, amount: int):
|
||||
await self._top_up_credits(user_id, amount)
|
||||
|
||||
async def onboarding_reward(self, user_id: str, credits: int, step: OnboardingStep):
|
||||
key = f"REWARD-{user_id}-{step.value}"
|
||||
if not await CreditTransaction.prisma().find_first(
|
||||
where={
|
||||
"userId": user_id,
|
||||
"transactionKey": key,
|
||||
}
|
||||
):
|
||||
await self._add_transaction(
|
||||
user_id=user_id,
|
||||
amount=credits,
|
||||
transaction_type=CreditTransactionType.GRANT,
|
||||
transaction_key=key,
|
||||
metadata=Json(
|
||||
{"reason": f"Reward for completing {step.value} onboarding step."}
|
||||
),
|
||||
)
|
||||
|
||||
async def top_up_refund(
|
||||
self, user_id: str, transaction_key: str, metadata: dict[str, str]
|
||||
) -> int:
|
||||
@@ -895,6 +926,9 @@ class DisabledUserCredit(UserCreditBase):
|
||||
async def top_up_credits(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
async def onboarding_reward(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
async def top_up_intent(self, *args, **kwargs) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ from prisma.enums import OnboardingStep
|
||||
from prisma.models import UserOnboarding
|
||||
from prisma.types import UserOnboardingCreateInput, UserOnboardingUpdateInput
|
||||
|
||||
from backend.data import db
|
||||
from backend.data.block import get_blocks
|
||||
from backend.data.credit import get_user_credit_model
|
||||
from backend.data.graph import GraphModel
|
||||
from backend.data.model import CredentialsMetaInput
|
||||
from backend.server.v2.store.model import StoreAgentDetails
|
||||
@@ -24,14 +26,19 @@ 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
|
||||
|
||||
user_credit = get_user_credit_model()
|
||||
|
||||
|
||||
class UserOnboardingUpdate(pydantic.BaseModel):
|
||||
completedSteps: Optional[list[OnboardingStep]] = None
|
||||
notificationDot: Optional[bool] = None
|
||||
notified: Optional[list[OnboardingStep]] = None
|
||||
usageReason: Optional[str] = None
|
||||
integrations: Optional[list[str]] = None
|
||||
otherIntegrations: Optional[str] = None
|
||||
selectedStoreListingVersionId: Optional[str] = None
|
||||
agentInput: Optional[dict[str, Any]] = None
|
||||
onboardingAgentExecutionId: Optional[str] = None
|
||||
|
||||
|
||||
async def get_user_onboarding(user_id: str):
|
||||
@@ -48,6 +55,20 @@ async def update_user_onboarding(user_id: str, data: UserOnboardingUpdate):
|
||||
update: UserOnboardingUpdateInput = {}
|
||||
if data.completedSteps is not None:
|
||||
update["completedSteps"] = list(set(data.completedSteps))
|
||||
for step in (
|
||||
OnboardingStep.AGENT_NEW_RUN,
|
||||
OnboardingStep.GET_RESULTS,
|
||||
OnboardingStep.MARKETPLACE_ADD_AGENT,
|
||||
OnboardingStep.MARKETPLACE_RUN_AGENT,
|
||||
OnboardingStep.BUILDER_SAVE_AGENT,
|
||||
OnboardingStep.BUILDER_RUN_AGENT,
|
||||
):
|
||||
if step in data.completedSteps:
|
||||
await reward_user(user_id, step)
|
||||
if data.notificationDot is not None:
|
||||
update["notificationDot"] = data.notificationDot
|
||||
if data.notified is not None:
|
||||
update["notified"] = list(set(data.notified))
|
||||
if data.usageReason is not None:
|
||||
update["usageReason"] = data.usageReason
|
||||
if data.integrations is not None:
|
||||
@@ -58,6 +79,8 @@ async def update_user_onboarding(user_id: str, data: UserOnboardingUpdate):
|
||||
update["selectedStoreListingVersionId"] = data.selectedStoreListingVersionId
|
||||
if data.agentInput is not None:
|
||||
update["agentInput"] = Json(data.agentInput)
|
||||
if data.onboardingAgentExecutionId is not None:
|
||||
update["onboardingAgentExecutionId"] = data.onboardingAgentExecutionId
|
||||
|
||||
return await UserOnboarding.prisma().upsert(
|
||||
where={"userId": user_id},
|
||||
@@ -68,6 +91,45 @@ async def update_user_onboarding(user_id: str, data: UserOnboardingUpdate):
|
||||
)
|
||||
|
||||
|
||||
async def reward_user(user_id: str, step: OnboardingStep):
|
||||
async with db.locked_transaction(f"usr_trx_{user_id}-reward"):
|
||||
reward = 0
|
||||
match step:
|
||||
# Reward user when they clicked New Run during onboarding
|
||||
# This is because they need credits before scheduling a run (next step)
|
||||
case OnboardingStep.AGENT_NEW_RUN:
|
||||
reward = 300
|
||||
case OnboardingStep.GET_RESULTS:
|
||||
reward = 300
|
||||
case OnboardingStep.MARKETPLACE_ADD_AGENT:
|
||||
reward = 100
|
||||
case OnboardingStep.MARKETPLACE_RUN_AGENT:
|
||||
reward = 100
|
||||
case OnboardingStep.BUILDER_SAVE_AGENT:
|
||||
reward = 100
|
||||
case OnboardingStep.BUILDER_RUN_AGENT:
|
||||
reward = 100
|
||||
|
||||
if reward == 0:
|
||||
return
|
||||
|
||||
onboarding = await get_user_onboarding(user_id)
|
||||
|
||||
# Skip if already rewarded
|
||||
if step in onboarding.rewardedFor:
|
||||
return
|
||||
|
||||
onboarding.rewardedFor.append(step)
|
||||
await user_credit.onboarding_reward(user_id, reward, step)
|
||||
await UserOnboarding.prisma().update(
|
||||
where={"userId": user_id},
|
||||
data={
|
||||
"completedSteps": list(set(onboarding.completedSteps + [step])),
|
||||
"rewardedFor": onboarding.rewardedFor,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def clean_and_split(text: str) -> list[str]:
|
||||
"""
|
||||
Removes all special characters from a string, truncates it to 100 characters,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Modify the OnboardingStep enum
|
||||
ALTER TYPE "OnboardingStep" ADD VALUE 'GET_RESULTS';
|
||||
ALTER TYPE "OnboardingStep" ADD VALUE 'MARKETPLACE_VISIT';
|
||||
ALTER TYPE "OnboardingStep" ADD VALUE 'MARKETPLACE_ADD_AGENT';
|
||||
ALTER TYPE "OnboardingStep" ADD VALUE 'MARKETPLACE_RUN_AGENT';
|
||||
ALTER TYPE "OnboardingStep" ADD VALUE 'BUILDER_OPEN';
|
||||
ALTER TYPE "OnboardingStep" ADD VALUE 'BUILDER_SAVE_AGENT';
|
||||
ALTER TYPE "OnboardingStep" ADD VALUE 'BUILDER_RUN_AGENT';
|
||||
|
||||
-- Modify the UserOnboarding table
|
||||
ALTER TABLE "UserOnboarding"
|
||||
ADD COLUMN "updatedAt" TIMESTAMP(3),
|
||||
ADD COLUMN "notificationDot" BOOLEAN NOT NULL DEFAULT true,
|
||||
ADD COLUMN "notified" "OnboardingStep"[] DEFAULT '{}',
|
||||
ADD COLUMN "rewardedFor" "OnboardingStep"[] DEFAULT '{}',
|
||||
ADD COLUMN "onboardingAgentExecutionId" TEXT
|
||||
@@ -58,6 +58,7 @@ model User {
|
||||
}
|
||||
|
||||
enum OnboardingStep {
|
||||
// Introductory onboarding (Library)
|
||||
WELCOME
|
||||
USAGE_REASON
|
||||
INTEGRATIONS
|
||||
@@ -65,18 +66,32 @@ enum OnboardingStep {
|
||||
AGENT_NEW_RUN
|
||||
AGENT_INPUT
|
||||
CONGRATS
|
||||
GET_RESULTS
|
||||
// Marketplace
|
||||
MARKETPLACE_VISIT
|
||||
MARKETPLACE_ADD_AGENT
|
||||
MARKETPLACE_RUN_AGENT
|
||||
// Builder
|
||||
BUILDER_OPEN
|
||||
BUILDER_SAVE_AGENT
|
||||
BUILDER_RUN_AGENT
|
||||
}
|
||||
|
||||
model UserOnboarding {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime? @updatedAt
|
||||
|
||||
completedSteps OnboardingStep[] @default([])
|
||||
notificationDot Boolean @default(true)
|
||||
notified OnboardingStep[] @default([])
|
||||
rewardedFor OnboardingStep[] @default([])
|
||||
usageReason String?
|
||||
integrations String[] @default([])
|
||||
otherIntegrations String?
|
||||
selectedStoreListingVersionId String?
|
||||
agentInput Json?
|
||||
onboardingAgentExecutionId String?
|
||||
|
||||
userId String @unique
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
"@xyflow/react": "12.4.2",
|
||||
"ajv": "^8.17.1",
|
||||
"boring-avatars": "^1.11.2",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
@@ -70,6 +69,7 @@
|
||||
"moment": "^2.30.1",
|
||||
"next": "^14.2.26",
|
||||
"next-themes": "^0.4.5",
|
||||
"party-js": "^2.2.0",
|
||||
"react": "^18",
|
||||
"react-day-picker": "^9.6.1",
|
||||
"react-dom": "^18",
|
||||
|
||||
BIN
autogpt_platform/frontend/public/onboarding/builder-open.mp4
Normal file
BIN
autogpt_platform/frontend/public/onboarding/builder-open.mp4
Normal file
Binary file not shown.
BIN
autogpt_platform/frontend/public/onboarding/builder-run.mp4
Normal file
BIN
autogpt_platform/frontend/public/onboarding/builder-run.mp4
Normal file
Binary file not shown.
BIN
autogpt_platform/frontend/public/onboarding/builder-save.mp4
Normal file
BIN
autogpt_platform/frontend/public/onboarding/builder-save.mp4
Normal file
Binary file not shown.
BIN
autogpt_platform/frontend/public/onboarding/get-results.mp4
Normal file
BIN
autogpt_platform/frontend/public/onboarding/get-results.mp4
Normal file
Binary file not shown.
BIN
autogpt_platform/frontend/public/onboarding/marketplace-add.mp4
Normal file
BIN
autogpt_platform/frontend/public/onboarding/marketplace-add.mp4
Normal file
Binary file not shown.
BIN
autogpt_platform/frontend/public/onboarding/marketplace-run.mp4
Normal file
BIN
autogpt_platform/frontend/public/onboarding/marketplace-run.mp4
Normal file
Binary file not shown.
Binary file not shown.
@@ -3,9 +3,16 @@
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { GraphID } from "@/lib/autogpt-server-api/types";
|
||||
import FlowEditor from "@/components/Flow";
|
||||
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function Home() {
|
||||
const query = useSearchParams();
|
||||
const { completeStep } = useOnboarding();
|
||||
|
||||
useEffect(() => {
|
||||
completeStep("BUILDER_OPEN");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FlowEditor
|
||||
|
||||
@@ -22,6 +22,7 @@ import AgentRunDraftView from "@/components/agents/agent-run-draft-view";
|
||||
import AgentRunDetailsView from "@/components/agents/agent-run-details-view";
|
||||
import AgentRunsSelectorList from "@/components/agents/agent-runs-selector-list";
|
||||
import AgentScheduleDetailsView from "@/components/agents/agent-schedule-details-view";
|
||||
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
|
||||
|
||||
export default function AgentRunsPage(): React.ReactElement {
|
||||
const { id: agentID }: { id: LibraryAgentID } = useParams();
|
||||
@@ -49,6 +50,7 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
useState<boolean>(false);
|
||||
const [confirmingDeleteAgentRun, setConfirmingDeleteAgentRun] =
|
||||
useState<GraphExecutionMeta | null>(null);
|
||||
const { state, updateState } = useOnboarding();
|
||||
|
||||
const openRunDraftView = useCallback(() => {
|
||||
selectView({ type: "run" });
|
||||
@@ -78,6 +80,18 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
[api, graphVersions],
|
||||
);
|
||||
|
||||
// Reward user for viewing results of their onboarding agent
|
||||
useEffect(() => {
|
||||
if (!state || !selectedRun || state.completedSteps.includes("GET_RESULTS"))
|
||||
return;
|
||||
|
||||
if (selectedRun.id === state.onboardingAgentExecutionId) {
|
||||
updateState({
|
||||
completedSteps: [...state.completedSteps, "GET_RESULTS"],
|
||||
});
|
||||
}
|
||||
}, [selectedRun, state]);
|
||||
|
||||
const fetchAgents = useCallback(() => {
|
||||
api.getLibraryAgent(agentID).then((agent) => {
|
||||
setAgent(agent);
|
||||
|
||||
@@ -83,8 +83,14 @@ export default function Page() {
|
||||
api.addMarketplaceAgentToLibrary(
|
||||
storeAgent?.store_listing_version_id || "",
|
||||
);
|
||||
api.executeGraph(agent.id, agent.version, state?.agentInput || {});
|
||||
router.push("/onboarding/6-congrats");
|
||||
api
|
||||
.executeGraph(agent.id, agent.version, state?.agentInput || {})
|
||||
.then(({ graph_exec_id }) => {
|
||||
updateState({
|
||||
onboardingAgentExecutionId: graph_exec_id,
|
||||
});
|
||||
router.push("/onboarding/6-congrats");
|
||||
});
|
||||
}, [api, agent, router, state?.agentInput]);
|
||||
|
||||
const runYourAgent = (
|
||||
|
||||
@@ -6,9 +6,6 @@ import { redirect } from "next/navigation";
|
||||
export async function finishOnboarding() {
|
||||
const api = new BackendAPI();
|
||||
const onboarding = await api.getUserOnboarding();
|
||||
await api.updateUserOnboarding({
|
||||
completedSteps: [...onboarding.completedSteps, "CONGRATS"],
|
||||
});
|
||||
revalidatePath("/library", "layout");
|
||||
redirect("/library");
|
||||
}
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { finishOnboarding } from "./actions";
|
||||
import confetti from "canvas-confetti";
|
||||
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
|
||||
import * as party from "party-js";
|
||||
|
||||
export default function Page() {
|
||||
useOnboarding(7, "AGENT_INPUT");
|
||||
const { state, updateState } = useOnboarding(7, "AGENT_INPUT");
|
||||
const [showText, setShowText] = useState(false);
|
||||
const [showSubtext, setShowSubtext] = useState(false);
|
||||
const divRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
confetti({
|
||||
particleCount: 120,
|
||||
spread: 360,
|
||||
shapes: ["square", "circle"],
|
||||
scalar: 2,
|
||||
decay: 0.93,
|
||||
origin: { y: 0.38, x: 0.51 },
|
||||
});
|
||||
if (divRef.current) {
|
||||
party.confetti(divRef.current, {
|
||||
count: 100,
|
||||
spread: 180,
|
||||
shapes: ["square", "circle"],
|
||||
size: party.variation.range(2, 2), // scalar: 2
|
||||
speed: party.variation.range(300, 1000),
|
||||
});
|
||||
}
|
||||
|
||||
const timer0 = setTimeout(() => {
|
||||
setShowText(true);
|
||||
@@ -29,6 +31,9 @@ export default function Page() {
|
||||
}, 500);
|
||||
|
||||
const timer2 = setTimeout(() => {
|
||||
updateState({
|
||||
completedSteps: [...(state?.completedSteps || []), "CONGRATS"],
|
||||
});
|
||||
finishOnboarding();
|
||||
}, 3000);
|
||||
|
||||
@@ -42,6 +47,7 @@ export default function Page() {
|
||||
return (
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center bg-violet-100">
|
||||
<div
|
||||
ref={divRef}
|
||||
className={cn(
|
||||
"z-10 -mb-16 text-9xl duration-500",
|
||||
showText ? "opacity-100" : "opacity-0",
|
||||
@@ -63,7 +69,7 @@ export default function Page() {
|
||||
showSubtext ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
>
|
||||
You earned 15$ for running your first agent
|
||||
You earned 3$ for running your first agent
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,11 +5,14 @@ export default async function OnboardingResetPage() {
|
||||
const api = new BackendAPI();
|
||||
await api.updateUserOnboarding({
|
||||
completedSteps: [],
|
||||
notificationDot: true,
|
||||
notified: [],
|
||||
usageReason: null,
|
||||
integrations: [],
|
||||
otherIntegrations: "",
|
||||
selectedStoreListingVersionId: null,
|
||||
agentInput: {},
|
||||
onboardingAgentExecutionId: null,
|
||||
});
|
||||
redirect("/onboarding/1-welcome");
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useToastOnFail } from "@/components/ui/use-toast";
|
||||
import ActionButtonGroup from "@/components/agptui/action-button-group";
|
||||
import SchemaTooltip from "@/components/SchemaTooltip";
|
||||
import { IconPlay } from "@/components/ui/icons";
|
||||
import { useOnboarding } from "../onboarding/onboarding-provider";
|
||||
|
||||
export default function AgentRunDraftView({
|
||||
graph,
|
||||
@@ -26,15 +27,18 @@ export default function AgentRunDraftView({
|
||||
|
||||
const agentInputs = graph.input_schema.properties;
|
||||
const [inputValues, setInputValues] = useState<Record<string, any>>({});
|
||||
const { state, completeStep } = useOnboarding();
|
||||
|
||||
const doRun = useCallback(
|
||||
() =>
|
||||
api
|
||||
.executeGraph(graph.id, graph.version, inputValues)
|
||||
.then((newRun) => onRun(newRun.graph_exec_id))
|
||||
.catch(toastOnFail("execute agent")),
|
||||
[api, graph, inputValues, onRun, toastOnFail],
|
||||
);
|
||||
const doRun = useCallback(() => {
|
||||
api
|
||||
.executeGraph(graph.id, graph.version, inputValues)
|
||||
.then((newRun) => onRun(newRun.graph_exec_id))
|
||||
.catch(toastOnFail("execute agent"));
|
||||
// Mark run agent onboarding step as completed
|
||||
if (state?.completedSteps.includes("MARKETPLACE_ADD_AGENT")) {
|
||||
completeStep("MARKETPLACE_RUN_AGENT");
|
||||
}
|
||||
}, [api, graph, inputValues, onRun, state]);
|
||||
|
||||
const runActions: ButtonAction[] = useMemo(
|
||||
() => [
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useToast } from "@/components/ui/use-toast";
|
||||
|
||||
import useSupabase from "@/hooks/useSupabase";
|
||||
import { DownloadIcon, LoaderIcon } from "lucide-react";
|
||||
import { useOnboarding } from "../onboarding/onboarding-provider";
|
||||
interface AgentInfoProps {
|
||||
name: string;
|
||||
creator: string;
|
||||
@@ -39,6 +40,7 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
|
||||
const api = React.useMemo(() => new BackendAPI(), []);
|
||||
const { user } = useSupabase();
|
||||
const { toast } = useToast();
|
||||
const { completeStep } = useOnboarding();
|
||||
|
||||
const [downloading, setDownloading] = React.useState(false);
|
||||
|
||||
@@ -47,6 +49,7 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
|
||||
const newLibraryAgent = await api.addMarketplaceAgentToLibrary(
|
||||
storeListingVersionId,
|
||||
);
|
||||
completeStep("MARKETPLACE_ADD_AGENT");
|
||||
router.push(`/library/agents/${newLibraryAgent.id}`);
|
||||
} catch (error) {
|
||||
console.error("Failed to add agent to library:", error);
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import CreditsCard from "./CreditsCard";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
|
||||
const meta: Meta<typeof CreditsCard> = {
|
||||
title: "AGPT UI/Credits Card",
|
||||
component: CreditsCard,
|
||||
tags: ["autodocs"],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CreditsCard>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
credits: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const SmallNumber: Story = {
|
||||
args: {
|
||||
credits: 10,
|
||||
},
|
||||
};
|
||||
|
||||
export const LargeNumber: Story = {
|
||||
args: {
|
||||
credits: 1000000,
|
||||
},
|
||||
};
|
||||
|
||||
export const InteractionTest: Story = {
|
||||
args: {
|
||||
credits: 100,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const refreshButton = canvas.getByRole("button", {
|
||||
name: /refresh credits/i,
|
||||
});
|
||||
await userEvent.click(refreshButton);
|
||||
},
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { IconRefresh } from "@/components/ui/icons";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import useCredits from "@/hooks/useCredits";
|
||||
|
||||
const CreditsCard = () => {
|
||||
const { credits, formatCredits, fetchCredits } = useCredits({
|
||||
fetchInitialCredits: true,
|
||||
});
|
||||
const api = useBackendAPI();
|
||||
|
||||
const onRefresh = async () => {
|
||||
fetchCredits();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="inline-flex h-[48px] items-center gap-2.5 rounded-2xl bg-neutral-200 p-4 dark:bg-neutral-800">
|
||||
<div className="flex items-center gap-0.5">
|
||||
<span className="p-ui-semibold text-base leading-7 text-neutral-900 dark:text-neutral-50">
|
||||
Balance: {formatCredits(credits)}
|
||||
</span>
|
||||
</div>
|
||||
<Tooltip key="RefreshCredits" delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
className="h-6 w-6 transition-colors hover:text-neutral-700 dark:hover:text-neutral-300"
|
||||
aria-label="Refresh credits"
|
||||
>
|
||||
<IconRefresh className="h-6 w-6" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Refresh credits</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreditsCard;
|
||||
@@ -4,7 +4,7 @@ import { ProfilePopoutMenu } from "./ProfilePopoutMenu";
|
||||
import { IconType, IconLogIn, IconAutoGPTLogo } from "@/components/ui/icons";
|
||||
import { MobileNavBar } from "./MobileNavBar";
|
||||
import { Button } from "./Button";
|
||||
import CreditsCard from "./CreditsCard";
|
||||
import Wallet from "./Wallet";
|
||||
import { ProfileDetails } from "@/lib/autogpt-server-api/types";
|
||||
import { NavbarLink } from "./NavbarLink";
|
||||
import getServerUser from "@/lib/supabase/getServerUser";
|
||||
@@ -61,7 +61,7 @@ export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => {
|
||||
<div className="flex items-center gap-4">
|
||||
{isLoggedIn ? (
|
||||
<div className="flex items-center gap-4">
|
||||
{profile && <CreditsCard />}
|
||||
{profile && <Wallet />}
|
||||
<ProfilePopoutMenu
|
||||
menuItemGroups={menuItemGroups}
|
||||
userName={profile?.username}
|
||||
|
||||
114
autogpt_platform/frontend/src/components/agptui/Wallet.tsx
Normal file
114
autogpt_platform/frontend/src/components/agptui/Wallet.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
|
||||
import useCredits from "@/hooks/useCredits";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { X } from "lucide-react";
|
||||
import { PopoverClose } from "@radix-ui/react-popover";
|
||||
import { TaskGroups } from "../onboarding/WalletTaskGroups";
|
||||
import { ScrollArea } from "../ui/scroll-area";
|
||||
import { useOnboarding } from "../onboarding/onboarding-provider";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as party from "party-js";
|
||||
|
||||
export default function Wallet() {
|
||||
const { credits, formatCredits, fetchCredits } = useCredits({
|
||||
fetchInitialCredits: true,
|
||||
});
|
||||
const { state, updateState } = useOnboarding();
|
||||
const walletRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const onWalletOpen = useCallback(async () => {
|
||||
if (state?.notificationDot) {
|
||||
updateState({ notificationDot: false });
|
||||
}
|
||||
// Refresh credits when the wallet is opened
|
||||
fetchCredits();
|
||||
}, [state?.notificationDot, updateState, fetchCredits]);
|
||||
|
||||
const fadeOut = new party.ModuleBuilder()
|
||||
.drive("opacity")
|
||||
.by((t) => 1 - t)
|
||||
.through("lifetime")
|
||||
.build();
|
||||
|
||||
useEffect(() => {
|
||||
// Check if there are any completed tasks (state?.completedTasks) that
|
||||
// are not in the state?.notified array and play confetti if so
|
||||
const pending = state?.completedSteps
|
||||
.filter((step) => !state?.notified.includes(step))
|
||||
// Ignore steps that are not relevant for notifications
|
||||
.filter(
|
||||
(step) =>
|
||||
step !== "WELCOME" &&
|
||||
step !== "USAGE_REASON" &&
|
||||
step !== "INTEGRATIONS" &&
|
||||
step !== "AGENT_CHOICE" &&
|
||||
step !== "AGENT_NEW_RUN" &&
|
||||
step !== "AGENT_INPUT",
|
||||
);
|
||||
if ((pending?.length || 0) > 0 && walletRef.current) {
|
||||
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],
|
||||
});
|
||||
}
|
||||
}, [state?.completedSteps, state?.notified]);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
ref={walletRef}
|
||||
className="relative flex items-center gap-1 rounded-md bg-zinc-200 px-3 py-2 text-sm transition-colors duration-200 hover:bg-zinc-300"
|
||||
onClick={onWalletOpen}
|
||||
>
|
||||
Wallet{" "}
|
||||
<span className="text-sm font-semibold">
|
||||
{formatCredits(credits)}
|
||||
</span>
|
||||
{state?.notificationDot && (
|
||||
<span className="absolute right-1 top-1 h-2 w-2 rounded-full bg-violet-600"></span>
|
||||
)}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className={cn(
|
||||
"absolute -right-[7.9rem] -top-[3.2rem] z-50 w-[28.5rem] px-[0.625rem] py-2",
|
||||
"rounded-xl border-zinc-200 bg-zinc-50 shadow-[0_3px_3px] shadow-zinc-300",
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<div className="mx-1 flex items-center justify-between border-b border-zinc-300 pb-2">
|
||||
<span className="font-poppins font-medium text-zinc-900">
|
||||
Your wallet
|
||||
</span>
|
||||
<div className="flex items-center font-inter text-sm font-semibold text-violet-700">
|
||||
<div className="rounded-lg bg-violet-100 px-3 py-2">
|
||||
Wallet{" "}
|
||||
<span className="font-semibold">{formatCredits(credits)}</span>
|
||||
</div>
|
||||
<PopoverClose>
|
||||
<X className="ml-[2.8rem] h-5 w-5 text-zinc-800 hover:text-foreground" />
|
||||
</PopoverClose>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mx-1 mt-3 font-inter text-xs text-muted-foreground text-zinc-400">
|
||||
Complete the following tasks to earn more credits!
|
||||
</p>
|
||||
</div>
|
||||
<ScrollArea className="max-h-[80vh] overflow-y-auto">
|
||||
<TaskGroups />
|
||||
</ScrollArea>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -4,9 +4,16 @@ import * as React from "react";
|
||||
import { SearchBar } from "@/components/agptui/SearchBar";
|
||||
import { FilterChips } from "@/components/agptui/FilterChips";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
|
||||
|
||||
export const HeroSection: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { completeStep } = useOnboarding();
|
||||
|
||||
// Mark marketplace visit task as completed
|
||||
React.useEffect(() => {
|
||||
completeStep("MARKETPLACE_VISIT");
|
||||
}, [completeStep]);
|
||||
|
||||
function onFilterChange(selectedFilters: string[]) {
|
||||
const encodedTerm = encodeURIComponent(selectedFilters.join(", "));
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ChevronDown, Check } from "lucide-react";
|
||||
import { OnboardingStep } from "@/lib/autogpt-server-api";
|
||||
import { useOnboarding } from "./onboarding-provider";
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as party from "party-js";
|
||||
|
||||
interface Task {
|
||||
id: OnboardingStep;
|
||||
name: string;
|
||||
amount: number;
|
||||
details: string;
|
||||
video?: string;
|
||||
}
|
||||
|
||||
interface TaskGroup {
|
||||
name: string;
|
||||
tasks: Task[];
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export function TaskGroups() {
|
||||
const [groups, setGroups] = useState<TaskGroup[]>([
|
||||
{
|
||||
name: "Run your first agent",
|
||||
isOpen: false,
|
||||
tasks: [
|
||||
{
|
||||
id: "CONGRATS",
|
||||
name: "Finish onboarding",
|
||||
amount: 3,
|
||||
details: "Go through our step by step tutorial",
|
||||
},
|
||||
{
|
||||
id: "GET_RESULTS",
|
||||
name: "Get results from first agent",
|
||||
amount: 3,
|
||||
details:
|
||||
"Sit back and relax - your agent is running and will finish soon! See the results in the Library once it's done",
|
||||
video: "/onboarding/get-results.mp4",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Explore the Marketplace",
|
||||
isOpen: false,
|
||||
tasks: [
|
||||
{
|
||||
id: "MARKETPLACE_VISIT",
|
||||
name: "Go to Marketplace",
|
||||
amount: 0,
|
||||
details: "Click Marketplace in the top navigation",
|
||||
video: "/onboarding/marketplace-visit.mp4",
|
||||
},
|
||||
{
|
||||
id: "MARKETPLACE_ADD_AGENT",
|
||||
name: "Find an agent",
|
||||
amount: 1,
|
||||
details:
|
||||
"Search for an agent in the Marketplace, like a code generator or research assistant and add it to your Library",
|
||||
video: "/onboarding/marketplace-add.mp4",
|
||||
},
|
||||
{
|
||||
id: "MARKETPLACE_RUN_AGENT",
|
||||
name: "Try out your agent",
|
||||
amount: 1,
|
||||
details:
|
||||
"Run the agent you found in the Marketplace from the Library - whether it's a writing assistant, data analyzer, or something else",
|
||||
video: "/onboarding/marketplace-run.mp4",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Build your own agent",
|
||||
isOpen: false,
|
||||
tasks: [
|
||||
{
|
||||
id: "BUILDER_OPEN",
|
||||
name: "Open the Builder",
|
||||
amount: 0,
|
||||
details: "Click Builder in the top navigation",
|
||||
video: "/onboarding/builder-open.mp4",
|
||||
},
|
||||
{
|
||||
id: "BUILDER_SAVE_AGENT",
|
||||
name: "Place your first blocks and save your agent",
|
||||
amount: 1,
|
||||
details:
|
||||
"Open block library on the left and add a block to the canvas then save your agent",
|
||||
video: "/onboarding/builder-save.mp4",
|
||||
},
|
||||
{
|
||||
id: "BUILDER_RUN_AGENT",
|
||||
name: "Run your agent",
|
||||
amount: 1,
|
||||
details: "Run your agent from the Builder",
|
||||
video: "/onboarding/builder-run.mp4",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
const { state, updateState } = useOnboarding();
|
||||
const refs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||
|
||||
const toggleGroup = useCallback((name: string) => {
|
||||
setGroups((prevGroups) =>
|
||||
prevGroups.map((group) =>
|
||||
group.name === name ? { ...group, isOpen: !group.isOpen } : group,
|
||||
),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const isTaskCompleted = useCallback(
|
||||
(task: Task) => {
|
||||
return state?.completedSteps?.includes(task.id) || false;
|
||||
},
|
||||
[state?.completedSteps],
|
||||
);
|
||||
|
||||
const getCompletedCount = useCallback(
|
||||
(tasks: Task[]) => {
|
||||
return tasks.filter((task) => isTaskCompleted(task)).length;
|
||||
},
|
||||
[isTaskCompleted],
|
||||
);
|
||||
|
||||
const isGroupCompleted = useCallback(
|
||||
(group: TaskGroup) => {
|
||||
return group.tasks.every((task) => isTaskCompleted(task));
|
||||
},
|
||||
[isTaskCompleted],
|
||||
);
|
||||
|
||||
const setRef = (name: string) => (el: HTMLDivElement | null) => {
|
||||
if (el) {
|
||||
refs.current[name] = el;
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
if (groupCompleted) {
|
||||
const el = refs.current[group.name];
|
||||
if (el && !alreadyCelebrated) {
|
||||
party.confetti(el, {
|
||||
count: 50,
|
||||
spread: 120,
|
||||
shapes: ["square", "circle"],
|
||||
size: party.variation.range(1, 2),
|
||||
speed: party.variation.range(200, 300),
|
||||
});
|
||||
// Update the state to include all group tasks as notified
|
||||
// This ensures that the confetti effect isn't perpetually triggered on Wallet
|
||||
const notifiedTasks = group.tasks.map((task) => task.id);
|
||||
updateState({
|
||||
notified: [...(state?.notified || []), ...notifiedTasks],
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
group.tasks.forEach((task) => {
|
||||
const el = refs.current[task.id];
|
||||
if (el && isTaskCompleted(task) && !state?.notified.includes(task.id)) {
|
||||
party.confetti(el, {
|
||||
count: 40,
|
||||
spread: 120,
|
||||
shapes: ["square", "circle"],
|
||||
size: party.variation.range(1, 1.5),
|
||||
speed: party.variation.range(200, 300),
|
||||
});
|
||||
// Update the state to include the task as notified
|
||||
updateState({ notified: [...(state?.notified || []), task.id] });
|
||||
}
|
||||
});
|
||||
});
|
||||
}, [state?.completedSteps]);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{groups.map((group) => (
|
||||
<div
|
||||
key={group.name}
|
||||
ref={setRef(group.name)}
|
||||
className="mt-3 overflow-hidden rounded-lg border border-zinc-200 bg-zinc-100"
|
||||
>
|
||||
{/* Group Header - unchanged */}
|
||||
<div
|
||||
className="flex cursor-pointer items-center justify-between p-3"
|
||||
onClick={() => toggleGroup(group.name)}
|
||||
>
|
||||
{/* Name and completed count */}
|
||||
<div className="flex-1">
|
||||
<div
|
||||
className={cn(
|
||||
"text-sm font-medium text-zinc-900",
|
||||
isGroupCompleted(group) ? "text-zinc-600 line-through" : "",
|
||||
)}
|
||||
>
|
||||
{group.name}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-1 text-xs font-normal leading-tight text-zinc-500",
|
||||
isGroupCompleted(group) ? "line-through" : "",
|
||||
)}
|
||||
>
|
||||
{getCompletedCount(group.tasks)} of {group.tasks.length}{" "}
|
||||
completed
|
||||
</div>
|
||||
</div>
|
||||
{/* Reward and chevron */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
"text-xs font-medium leading-tight text-violet-600",
|
||||
isGroupCompleted(group) ? "line-through" : "",
|
||||
)}
|
||||
>
|
||||
$
|
||||
{group.tasks
|
||||
.reduce((sum, task) => sum + task.amount, 0)
|
||||
.toFixed(2)}
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={`h-5 w-5 text-slate-950 transition-transform duration-300 ease-in-out ${
|
||||
group.isOpen ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tasks */}
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-hidden transition-all duration-300 ease-in-out",
|
||||
group.isOpen || !isGroupCompleted(group)
|
||||
? "max-h-[1000px] opacity-100"
|
||||
: "max-h-0 opacity-0",
|
||||
)}
|
||||
>
|
||||
{group.tasks.map((task) => (
|
||||
<div
|
||||
key={task.id}
|
||||
ref={setRef(task.id)}
|
||||
className="mx-3 border-t border-zinc-300 px-1 pb-1 pt-3"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Checkmark and name */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-4 w-4 items-center justify-center rounded-full border",
|
||||
isTaskCompleted(task)
|
||||
? "border-emerald-600"
|
||||
: "border-zinc-600",
|
||||
)}
|
||||
>
|
||||
{isTaskCompleted(task) && (
|
||||
<Check className="h-3 w-3 text-emerald-600" />
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm font-normal",
|
||||
isTaskCompleted(task)
|
||||
? "text-zinc-500 line-through"
|
||||
: "text-zinc-800",
|
||||
)}
|
||||
>
|
||||
{task.name}
|
||||
</span>
|
||||
</div>
|
||||
{/* Reward */}
|
||||
{task.amount > 0 && (
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs font-normal text-zinc-500",
|
||||
isTaskCompleted(task) ? "line-through" : "",
|
||||
)}
|
||||
>
|
||||
${task.amount.toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Details section */}
|
||||
<div
|
||||
className={cn(
|
||||
"mt-2 overflow-hidden pl-6 text-xs font-normal text-zinc-500 transition-all duration-300 ease-in-out",
|
||||
isTaskCompleted(task) && "line-through",
|
||||
group.isOpen
|
||||
? "max-h-[100px] opacity-100"
|
||||
: "max-h-0 opacity-0",
|
||||
)}
|
||||
>
|
||||
{task.details}
|
||||
</div>
|
||||
{task.video && (
|
||||
<div
|
||||
className={cn(
|
||||
"relative mx-6 aspect-video overflow-hidden rounded-lg transition-all duration-300 ease-in-out",
|
||||
group.isOpen
|
||||
? "my-2 max-h-[200px] opacity-100"
|
||||
: "max-h-0 opacity-0",
|
||||
)}
|
||||
>
|
||||
<video
|
||||
src={task.video}
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
className={cn("h-full w-full object-cover object-center")}
|
||||
></video>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,9 +14,12 @@ import {
|
||||
const OnboardingContext = createContext<
|
||||
| {
|
||||
state: UserOnboarding | null;
|
||||
updateState: (state: Partial<UserOnboarding>) => void;
|
||||
updateState: (
|
||||
state: Omit<Partial<UserOnboarding>, "rewardedFor">,
|
||||
) => void;
|
||||
step: number;
|
||||
setStep: (step: number) => void;
|
||||
completeStep: (step: OnboardingStep) => void;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
@@ -84,19 +87,23 @@ export default function OnboardingProvider({
|
||||
}, [api, pathname, router]);
|
||||
|
||||
const updateState = useCallback(
|
||||
(newState: Partial<UserOnboarding>) => {
|
||||
(newState: Omit<Partial<UserOnboarding>, "rewardedFor">) => {
|
||||
setState((prev) => {
|
||||
api.updateUserOnboarding({ ...prev, ...newState });
|
||||
api.updateUserOnboarding(newState);
|
||||
|
||||
if (!prev) {
|
||||
// Handle initial state
|
||||
return {
|
||||
completedSteps: [],
|
||||
notificationDot: false,
|
||||
notified: [],
|
||||
rewardedFor: [],
|
||||
usageReason: null,
|
||||
integrations: [],
|
||||
otherIntegrations: null,
|
||||
selectedStoreListingVersionId: null,
|
||||
agentInput: null,
|
||||
onboardingAgentExecutionId: null,
|
||||
...newState,
|
||||
};
|
||||
}
|
||||
@@ -106,8 +113,21 @@ export default function OnboardingProvider({
|
||||
[api, setState],
|
||||
);
|
||||
|
||||
const completeStep = useCallback(
|
||||
(step: OnboardingStep) => {
|
||||
if (!state || state.completedSteps.includes(step)) return;
|
||||
|
||||
updateState({
|
||||
completedSteps: [...state.completedSteps, step],
|
||||
});
|
||||
},
|
||||
[api, state],
|
||||
);
|
||||
|
||||
return (
|
||||
<OnboardingContext.Provider value={{ state, updateState, step, setStep }}>
|
||||
<OnboardingContext.Provider
|
||||
value={{ state, updateState, step, setStep, completeStep }}
|
||||
>
|
||||
{children}
|
||||
</OnboardingContext.Provider>
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useToast } from "@/components/ui/use-toast";
|
||||
import { InputItem } from "@/components/RunnerUIWrapper";
|
||||
import { GraphMeta } from "@/lib/autogpt-server-api";
|
||||
import { default as NextLink } from "next/link";
|
||||
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
|
||||
|
||||
const ajv = new Ajv({ strict: false, allErrors: true });
|
||||
|
||||
@@ -77,6 +78,7 @@ export default function useAgentGraph(
|
||||
useState(false);
|
||||
const [nodes, setNodes] = useState<CustomNode[]>([]);
|
||||
const [edges, setEdges] = useState<CustomEdge[]>([]);
|
||||
const { state, completeStep } = useOnboarding();
|
||||
|
||||
const api = useMemo(
|
||||
() => new BackendAPI(process.env.NEXT_PUBLIC_AGPT_SERVER_URL!),
|
||||
@@ -576,6 +578,9 @@ export default function useAgentGraph(
|
||||
path.set("flowVersion", savedAgent.version.toString());
|
||||
path.set("flowExecutionID", graphExecution.graph_exec_id);
|
||||
router.push(`${pathname}?${path.toString()}`);
|
||||
if (state?.completedSteps.includes("BUILDER_SAVE_AGENT")) {
|
||||
completeStep("BUILDER_RUN_AGENT");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMessage =
|
||||
@@ -966,6 +971,7 @@ export default function useAgentGraph(
|
||||
const saveAgent = useCallback(async () => {
|
||||
try {
|
||||
await _saveAgent();
|
||||
completeStep("BUILDER_SAVE_AGENT");
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
@@ -180,7 +180,9 @@ export default class BackendAPI {
|
||||
return this._get("/onboarding");
|
||||
}
|
||||
|
||||
updateUserOnboarding(onboarding: Partial<UserOnboarding>): Promise<void> {
|
||||
updateUserOnboarding(
|
||||
onboarding: Omit<Partial<UserOnboarding>, "rewardedFor">,
|
||||
): Promise<void> {
|
||||
return this._request("PATCH", "/onboarding", onboarding);
|
||||
}
|
||||
|
||||
|
||||
@@ -801,15 +801,26 @@ export type OnboardingStep =
|
||||
| "AGENT_CHOICE"
|
||||
| "AGENT_NEW_RUN"
|
||||
| "AGENT_INPUT"
|
||||
| "CONGRATS";
|
||||
| "CONGRATS"
|
||||
| "GET_RESULTS"
|
||||
| "MARKETPLACE_VISIT"
|
||||
| "MARKETPLACE_ADD_AGENT"
|
||||
| "MARKETPLACE_RUN_AGENT"
|
||||
| "BUILDER_OPEN"
|
||||
| "BUILDER_SAVE_AGENT"
|
||||
| "BUILDER_RUN_AGENT";
|
||||
|
||||
export interface UserOnboarding {
|
||||
completedSteps: OnboardingStep[];
|
||||
notificationDot: boolean;
|
||||
notified: OnboardingStep[];
|
||||
rewardedFor: OnboardingStep[];
|
||||
usageReason: string | null;
|
||||
integrations: string[];
|
||||
otherIntegrations: string | null;
|
||||
selectedStoreListingVersionId: string | null;
|
||||
agentInput: { [key: string]: string } | null;
|
||||
agentInput: { [key: string]: string | number } | null;
|
||||
onboardingAgentExecutionId: GraphExecutionID | null;
|
||||
}
|
||||
|
||||
/* *** UTILITIES *** */
|
||||
|
||||
@@ -5026,11 +5026,6 @@ caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001688:
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001697.tgz#040bbbb54463c4b4b3377c716b34a322d16e6fc7"
|
||||
integrity sha512-GwNPlWJin8E+d7Gxq96jxM6w0w+VFeyyXRsjU58emtkYqnbwHqXm5uT2uCmO0RQE9htWknOP4xtBlLmM/gWxvQ==
|
||||
|
||||
canvas-confetti@^1.9.3:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/canvas-confetti/-/canvas-confetti-1.9.3.tgz#ef4c857420ad8045ab4abe8547261c8cdf229845"
|
||||
integrity sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==
|
||||
|
||||
case-sensitive-paths-webpack-plugin@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4"
|
||||
@@ -9590,6 +9585,11 @@ parse-passwd@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
|
||||
integrity sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==
|
||||
|
||||
party-js@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/party-js/-/party-js-2.2.0.tgz#3340026971c9e62fd34db102daaa645fbc9130b8"
|
||||
integrity sha512-50hGuALCpvDTrQLPQ1fgUgxKIWAH28ShVkmeK/3zhO0YJyCqkhrZhQEkWPxDYLvbFJ7YAXyROmFEu35gKpZLtQ==
|
||||
|
||||
pascal-case@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb"
|
||||
|
||||
Reference in New Issue
Block a user