mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 15:17:59 -05:00
feat(platform): WebSocket Onboarding notifications (#11335)
Use WebSocket notifications from the backend to display confetti. ### Changes 🏗️ - Send WebSocket notifications to the browser when new onboarding steps are completed - Handle WebSocket notifications events in the Wallet and use them instead of frontend-based logic to play confetti (fixes confetti appearing on every refresh) - Scroll to newly completed tasks when wallet opens just before confetti plays - Fix: make `Run again` button complete `RE_RUN_AGENT` task ### 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] Confetti are displayed when previously uncompleted tasks are completed - [x] Confetti do not appear on page refresh - [x] Wallet scrolls on open before confetti is displayed - [x] `Run again` button completes `RE_RUN_AGENT` task
This commit is contained in:
committed by
GitHub
parent
be01a1316a
commit
27d886f05c
@@ -11,6 +11,11 @@ from prisma.types import UserOnboardingCreateInput, UserOnboardingUpdateInput
|
||||
from backend.data.block import get_blocks
|
||||
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.server.model import OnboardingNotificationPayload
|
||||
from backend.server.v2.store.model import StoreAgentDetails
|
||||
from backend.util.cache import cached
|
||||
from backend.util.json import SafeJson
|
||||
@@ -82,22 +87,10 @@ async def update_user_onboarding(user_id: str, data: UserOnboardingUpdate):
|
||||
update["completedSteps"] = list(
|
||||
set(data.completedSteps + onboarding.completedSteps)
|
||||
)
|
||||
for step in (
|
||||
OnboardingStep.AGENT_NEW_RUN,
|
||||
OnboardingStep.MARKETPLACE_VISIT,
|
||||
OnboardingStep.MARKETPLACE_ADD_AGENT,
|
||||
OnboardingStep.MARKETPLACE_RUN_AGENT,
|
||||
OnboardingStep.BUILDER_SAVE_AGENT,
|
||||
OnboardingStep.RE_RUN_AGENT,
|
||||
OnboardingStep.SCHEDULE_AGENT,
|
||||
OnboardingStep.RUN_AGENTS,
|
||||
OnboardingStep.RUN_3_DAYS,
|
||||
OnboardingStep.TRIGGER_WEBHOOK,
|
||||
OnboardingStep.RUN_14_DAYS,
|
||||
OnboardingStep.RUN_AGENTS_100,
|
||||
):
|
||||
if step in data.completedSteps:
|
||||
await reward_user(user_id, step, onboarding)
|
||||
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:
|
||||
@@ -130,7 +123,7 @@ async def update_user_onboarding(user_id: str, data: UserOnboardingUpdate):
|
||||
)
|
||||
|
||||
|
||||
async def reward_user(user_id: str, step: OnboardingStep, onboarding: UserOnboarding):
|
||||
async def _reward_user(user_id: str, onboarding: UserOnboarding, step: OnboardingStep):
|
||||
reward = 0
|
||||
match step:
|
||||
# Reward user when they clicked New Run during onboarding
|
||||
@@ -180,20 +173,32 @@ async def reward_user(user_id: str, step: OnboardingStep, onboarding: UserOnboar
|
||||
)
|
||||
|
||||
|
||||
async def complete_webhook_trigger_step(user_id: str):
|
||||
async def complete_onboarding_step(user_id: str, step: OnboardingStep):
|
||||
"""
|
||||
Completes the TRIGGER_WEBHOOK onboarding step for the user if not already completed.
|
||||
Completes the specified onboarding step for the user if not already completed.
|
||||
"""
|
||||
|
||||
onboarding = await get_user_onboarding(user_id)
|
||||
if OnboardingStep.TRIGGER_WEBHOOK not in onboarding.completedSteps:
|
||||
if step not in onboarding.completedSteps:
|
||||
await update_user_onboarding(
|
||||
user_id,
|
||||
UserOnboardingUpdate(
|
||||
completedSteps=onboarding.completedSteps
|
||||
+ [OnboardingStep.TRIGGER_WEBHOOK]
|
||||
),
|
||||
UserOnboardingUpdate(completedSteps=onboarding.completedSteps + [step]),
|
||||
)
|
||||
await _send_onboarding_notification(user_id, step)
|
||||
|
||||
|
||||
async def _send_onboarding_notification(user_id: str, step: OnboardingStep):
|
||||
"""
|
||||
Sends an onboarding notification to the user for the specified step.
|
||||
"""
|
||||
payload = OnboardingNotificationPayload(
|
||||
type="onboarding",
|
||||
event="step_completed",
|
||||
step=step.value,
|
||||
)
|
||||
await AsyncRedisNotificationEventBus().publish(
|
||||
NotificationEvent(user_id=user_id, payload=payload)
|
||||
)
|
||||
|
||||
|
||||
def clean_and_split(text: str) -> list[str]:
|
||||
|
||||
@@ -33,7 +33,7 @@ from backend.data.model import (
|
||||
OAuth2Credentials,
|
||||
UserIntegrations,
|
||||
)
|
||||
from backend.data.onboarding import complete_webhook_trigger_step
|
||||
from backend.data.onboarding import OnboardingStep, complete_onboarding_step
|
||||
from backend.data.user import get_user_integrations
|
||||
from backend.executor.utils import add_graph_execution
|
||||
from backend.integrations.ayrshare import AyrshareClient, SocialPlatform
|
||||
@@ -376,7 +376,7 @@ async def webhook_ingress_generic(
|
||||
if not (webhook.triggered_nodes or webhook.triggered_presets):
|
||||
return
|
||||
|
||||
await complete_webhook_trigger_step(user_id)
|
||||
await complete_onboarding_step(user_id, OnboardingStep.TRIGGER_WEBHOOK)
|
||||
|
||||
# Execute all triggers concurrently for better performance
|
||||
tasks = []
|
||||
|
||||
@@ -38,6 +38,7 @@ 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";
|
||||
|
||||
export function AgentRunDetailsView({
|
||||
agent,
|
||||
@@ -64,6 +65,8 @@ export function AgentRunDetailsView({
|
||||
[run],
|
||||
);
|
||||
|
||||
const { completeStep } = useOnboarding();
|
||||
|
||||
const toastOnFail = useToastOnFail();
|
||||
|
||||
const infoStats: { label: string; value: React.ReactNode }[] = useMemo(() => {
|
||||
@@ -154,6 +157,7 @@ export function AgentRunDetailsView({
|
||||
name: graph.name,
|
||||
id: graph.id,
|
||||
});
|
||||
completeStep("RE_RUN_AGENT");
|
||||
onRun(id);
|
||||
})
|
||||
.catch(toastOnFail("execute agent"));
|
||||
|
||||
@@ -8,7 +8,10 @@ import {
|
||||
import { ScrollArea } from "@/components/__legacy__/ui/scroll-area";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import useCredits from "@/hooks/useCredits";
|
||||
import { OnboardingStep } from "@/lib/autogpt-server-api";
|
||||
import {
|
||||
OnboardingStep,
|
||||
WebSocketNotification,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
|
||||
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
|
||||
@@ -20,6 +23,7 @@ import * as party from "party-js";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { WalletRefill } from "./components/WalletRefill";
|
||||
import { TaskGroups } from "./components/WalletTaskGroups";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
|
||||
export interface Task {
|
||||
id: OnboardingStep;
|
||||
@@ -163,6 +167,7 @@ export function Wallet() {
|
||||
];
|
||||
}, [state]);
|
||||
|
||||
const api = useBackendAPI();
|
||||
const { credits, formatCredits, fetchCredits } = useCredits({
|
||||
fetchInitialCredits: true,
|
||||
});
|
||||
@@ -176,12 +181,8 @@ export function Wallet() {
|
||||
return groups.reduce((acc, group) => acc + group.tasks.length, 0);
|
||||
}, [groups]);
|
||||
|
||||
// Get total completed count for all groups
|
||||
// Total completed task count across all groups
|
||||
const [completedCount, setCompletedCount] = useState<number | null>(null);
|
||||
// Needed to show confetti when a new step is completed
|
||||
const [prevCompletedCount, setPrevCompletedCount] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const walletRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
@@ -250,47 +251,45 @@ export function Wallet() {
|
||||
);
|
||||
|
||||
// Confetti effect on the wallet button
|
||||
const handleNotification = useCallback(
|
||||
(notification: WebSocketNotification) => {
|
||||
if (notification.type !== "onboarding") {
|
||||
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],
|
||||
});
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// WebSocket setup for onboarding notifications
|
||||
useEffect(() => {
|
||||
// It's enough to check completed count,
|
||||
// because the order of completed steps is not important
|
||||
// If the count is the same, we don't need to do anything
|
||||
if (completedCount === null || completedCount === prevCompletedCount) {
|
||||
return;
|
||||
}
|
||||
// Otherwise, we need to set the new prevCompletedCount
|
||||
setPrevCompletedCount(completedCount);
|
||||
// If there was no previous count, we don't show confetti
|
||||
if (prevCompletedCount === null || !walletRef.current) {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
fetchCredits();
|
||||
if (!walletRef.current) {
|
||||
return;
|
||||
}
|
||||
// Fix confetti appearing in the top left corner
|
||||
const rect = walletRef.current.getBoundingClientRect();
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
return;
|
||||
}
|
||||
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],
|
||||
});
|
||||
}, 800);
|
||||
}, [
|
||||
state?.completedSteps,
|
||||
state?.notified,
|
||||
fadeOut,
|
||||
fetchCredits,
|
||||
completedCount,
|
||||
prevCompletedCount,
|
||||
walletRef,
|
||||
]);
|
||||
const detachMessage = api.onWebSocketMessage(
|
||||
"notification",
|
||||
handleNotification,
|
||||
);
|
||||
|
||||
api.connectWebSocket();
|
||||
|
||||
return () => {
|
||||
detachMessage();
|
||||
};
|
||||
}, [api, handleNotification]);
|
||||
|
||||
// Wallet flash on credits change
|
||||
useEffect(() => {
|
||||
|
||||
@@ -70,6 +70,14 @@ export function TaskGroups({ groups }: Props) {
|
||||
}
|
||||
};
|
||||
|
||||
const scrollIntoViewCentered = useCallback((el: HTMLDivElement) => {
|
||||
el.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
inline: "nearest",
|
||||
});
|
||||
}, []);
|
||||
|
||||
const delayConfetti = useCallback((el: HTMLDivElement, count: number) => {
|
||||
setTimeout(() => {
|
||||
if (!el) return;
|
||||
@@ -101,7 +109,8 @@ export function TaskGroups({ groups }: Props) {
|
||||
if (groupCompleted) {
|
||||
const el = refs.current[group.name];
|
||||
if (el && !alreadyCelebrated) {
|
||||
delayConfetti(el, 50);
|
||||
scrollIntoViewCentered(el);
|
||||
delayConfetti(el, 600);
|
||||
// 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);
|
||||
@@ -115,7 +124,8 @@ export function TaskGroups({ groups }: Props) {
|
||||
group.tasks.forEach((task) => {
|
||||
const el = refs.current[task.id];
|
||||
if (el && isTaskCompleted(task) && !state?.notified.includes(task.id)) {
|
||||
delayConfetti(el, 40);
|
||||
scrollIntoViewCentered(el);
|
||||
delayConfetti(el, 400);
|
||||
// Update the state to include the task as notified
|
||||
updateState({ notified: [...(state?.notified || []), task.id] });
|
||||
}
|
||||
@@ -129,6 +139,7 @@ export function TaskGroups({ groups }: Props) {
|
||||
state?.notified,
|
||||
isGroupCompleted,
|
||||
isTaskCompleted,
|
||||
scrollIntoViewCentered,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user