Skip to content

Commit 6f15782

Browse files
kczemajdyz
andauthored
feat(platform): Update Marketplace Agent listing buttons (#9843)
Currently agent listing on Marketplace have bad UX. ### Changes 🏗️ - Add function and endpoint to check if user has `LibraryAgent` by given `storeListingVersionId` - Redesign listing buttons - `Add to library` shown when user is logged in and doesn't have an agent in library - `See runs` shown when user logged in as has the agent in the library - `Download agent` always shown - Disabled buttons during processing (adding/downloading) - Stop raising when owner is trying to add own agent. Now it'll simply redirect to Library. - Remove button appearing/flickering after a delay on listing page - logged in status is now checked in server component. - Show error toast on adding/redirecting to library and downloading error - Update breadcrumbs and page title to say `Marketplace` instead of `Store` - `font-geist` -> `font-sans` (`font-geist` var doesn't exist) ### 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] Button on a listing is `Add to library` (no library agent) - [x] Agent can be added and user is redirected - [x] Button on the listing is `See runs` and clicking it redirects to the library agent - [x] Remove agent from library - [x] Buttons shows `Add to library` again - [x] Agent can be re-added - [x] Agent can be downloaded - [x] `Add to library` Button is hidden when user is logged out --------- Co-authored-by: Zamil Majdy <[email protected]>
1 parent 79319ad commit 6f15782

File tree

6 files changed

+160
-60
lines changed

6 files changed

+160
-60
lines changed

autogpt_platform/backend/backend/server/v2/library/db.py

+39-6
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,44 @@ async def get_library_agent(id: str, user_id: str) -> library_model.LibraryAgent
175175
raise store_exceptions.DatabaseError("Failed to fetch library agent") from e
176176

177177

178+
async def get_library_agent_by_store_version_id(
179+
store_listing_version_id: str,
180+
user_id: str,
181+
):
182+
"""
183+
Get the library agent metadata for a given store listing version ID and user ID.
184+
"""
185+
logger.debug(
186+
f"Getting library agent for store listing ID: {store_listing_version_id}"
187+
)
188+
189+
store_listing_version = (
190+
await prisma.models.StoreListingVersion.prisma().find_unique(
191+
where={"id": store_listing_version_id},
192+
)
193+
)
194+
if not store_listing_version:
195+
logger.warning(f"Store listing version not found: {store_listing_version_id}")
196+
raise store_exceptions.AgentNotFoundError(
197+
f"Store listing version {store_listing_version_id} not found or invalid"
198+
)
199+
200+
# Check if user already has this agent
201+
agent = await prisma.models.LibraryAgent.prisma().find_first(
202+
where={
203+
"userId": user_id,
204+
"agentGraphId": store_listing_version.agentGraphId,
205+
"agentGraphVersion": store_listing_version.agentGraphVersion,
206+
"isDeleted": False,
207+
},
208+
include={"AgentGraph": True},
209+
)
210+
if agent:
211+
return library_model.LibraryAgent.from_db(agent)
212+
else:
213+
return None
214+
215+
178216
async def add_generated_agent_image(
179217
graph: backend.data.graph.GraphModel,
180218
library_agent_id: str,
@@ -397,11 +435,6 @@ async def add_store_agent_to_library(
397435
)
398436

399437
graph = store_listing_version.AgentGraph
400-
if graph.userId == user_id:
401-
logger.warning(
402-
f"User #{user_id} attempted to add their own agent to their library"
403-
)
404-
raise store_exceptions.DatabaseError("Cannot add own agent to library")
405438

406439
# Check if user already has this agent
407440
existing_library_agent = (
@@ -411,7 +444,7 @@ async def add_store_agent_to_library(
411444
"agentGraphId": graph.id,
412445
"agentGraphVersion": graph.version,
413446
},
414-
include=library_agent_include(user_id),
447+
include={"AgentGraph": True},
415448
)
416449
)
417450
if existing_library_agent:

autogpt_platform/backend/backend/server/v2/library/routes/agents.py

+24
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,30 @@ async def get_library_agent(
8585
return await library_db.get_library_agent(id=library_agent_id, user_id=user_id)
8686

8787

88+
@router.get(
89+
"/marketplace/{store_listing_version_id}/",
90+
tags=["store, library"],
91+
response_model=library_model.LibraryAgent | None,
92+
)
93+
async def get_library_agent_by_store_listing_version_id(
94+
store_listing_version_id: str,
95+
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
96+
):
97+
"""
98+
Get Library Agent from Store Listing Version ID.
99+
"""
100+
try:
101+
return await library_db.get_library_agent_by_store_version_id(
102+
store_listing_version_id, user_id
103+
)
104+
except Exception as e:
105+
logger.error(f"Could not fetch library agent from store version ID: {e}")
106+
raise HTTPException(
107+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
108+
detail="Failed to add agent to library",
109+
) from e
110+
111+
88112
@router.post(
89113
"",
90114
status_code=status.HTTP_201_CREATED,

autogpt_platform/frontend/src/app/(platform)/marketplace/agent/[creator]/[slug]/page.tsx

+13-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AgentsSection } from "@/components/agptui/composite/AgentsSection";
66
import { BecomeACreator } from "@/components/agptui/BecomeACreator";
77
import { Separator } from "@/components/ui/separator";
88
import { Metadata } from "next";
9+
import getServerSupabase from "@/lib/supabase/getServerSupabase";
910

1011
export async function generateMetadata({
1112
params,
@@ -16,7 +17,7 @@ export async function generateMetadata({
1617
const agent = await api.getStoreAgent(params.creator, params.slug);
1718

1819
return {
19-
title: `${agent.agent_name} - AutoGPT Store`,
20+
title: `${agent.agent_name} - AutoGPT Marketplace`,
2021
description: agent.description,
2122
};
2223
}
@@ -43,9 +44,17 @@ export default async function Page({
4344
// We are using slug as we know its has been sanitized and is not null
4445
search_query: agent.slug.replace(/-/g, " "),
4546
});
47+
const {
48+
data: { user },
49+
} = await getServerSupabase().auth.getUser();
50+
const libraryAgent = user
51+
? await api.getLibraryAgentByStoreListingVersionID(
52+
agent.store_listing_version_id,
53+
)
54+
: null;
4655

4756
const breadcrumbs = [
48-
{ name: "Store", link: "/marketplace" },
57+
{ name: "Marketplace", link: "/marketplace" },
4958
{
5059
name: agent.creator,
5160
link: `/marketplace/creator/${encodeURIComponent(agent.creator)}`,
@@ -61,6 +70,7 @@ export default async function Page({
6170
<div className="mt-4 flex flex-col items-start gap-4 sm:mt-6 sm:gap-6 md:mt-8 md:flex-row md:gap-8">
6271
<div className="w-full md:w-auto md:shrink-0">
6372
<AgentInfo
73+
user={user}
6474
name={agent.agent_name}
6575
creator={agent.creator}
6676
shortDescription={agent.sub_heading}
@@ -71,6 +81,7 @@ export default async function Page({
7181
lastUpdated={agent.updated_at}
7282
version={agent.versions[agent.versions.length - 1]}
7383
storeListingVersionId={agent.store_listing_version_id}
84+
libraryAgent={libraryAgent}
7485
/>
7586
</div>
7687
<AgentImages

autogpt_platform/frontend/src/components/agptui/AgentInfo.stories.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ type Story = StoryObj<typeof meta>;
2727

2828
export const Default: Story = {
2929
args: {
30+
user: null,
31+
libraryAgent: null,
3032
name: "AI Video Generator",
3133
storeListingVersionId: "123",
3234
creator: "Toran Richards",

autogpt_platform/frontend/src/components/agptui/AgentInfo.tsx

+76-52
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
"use client";
22

3-
import * as React from "react";
4-
import { IconPlay, StarRatingIcons } from "@/components/ui/icons";
3+
import { StarRatingIcons } from "@/components/ui/icons";
54
import { Separator } from "@/components/ui/separator";
6-
import BackendAPI from "@/lib/autogpt-server-api";
5+
import BackendAPI, { LibraryAgent } from "@/lib/autogpt-server-api";
76
import { useRouter } from "next/navigation";
87
import Link from "next/link";
98
import { useToast } from "@/components/ui/use-toast";
109

11-
import useSupabase from "@/hooks/useSupabase";
12-
import { DownloadIcon, LoaderIcon } from "lucide-react";
1310
import { useOnboarding } from "../onboarding/onboarding-provider";
11+
import { User } from "@supabase/supabase-js";
12+
import { cn } from "@/lib/utils";
13+
import { FC, useCallback, useMemo, useState } from "react";
14+
1415
interface AgentInfoProps {
16+
user: User | null;
1517
name: string;
1618
creator: string;
1719
shortDescription: string;
@@ -22,9 +24,11 @@ interface AgentInfoProps {
2224
lastUpdated: string;
2325
version: string;
2426
storeListingVersionId: string;
27+
libraryAgent: LibraryAgent | null;
2528
}
2629

27-
export const AgentInfo: React.FC<AgentInfoProps> = ({
30+
export const AgentInfo: FC<AgentInfoProps> = ({
31+
user,
2832
name,
2933
creator,
3034
shortDescription,
@@ -35,28 +39,48 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
3539
lastUpdated,
3640
version,
3741
storeListingVersionId,
42+
libraryAgent,
3843
}) => {
3944
const router = useRouter();
40-
const api = React.useMemo(() => new BackendAPI(), []);
41-
const { user } = useSupabase();
45+
const api = useMemo(() => new BackendAPI(), []);
4246
const { toast } = useToast();
4347
const { completeStep } = useOnboarding();
44-
45-
const [downloading, setDownloading] = React.useState(false);
46-
47-
const handleAddToLibrary = async () => {
48+
const [adding, setAdding] = useState(false);
49+
const [downloading, setDownloading] = useState(false);
50+
51+
const libraryAction = useCallback(async () => {
52+
setAdding(true);
53+
if (libraryAgent) {
54+
toast({
55+
description: "Redirecting to your library...",
56+
duration: 2000,
57+
});
58+
// Redirect to the library agent page
59+
router.push(`/library/agents/${libraryAgent.id}`);
60+
return;
61+
}
4862
try {
4963
const newLibraryAgent = await api.addMarketplaceAgentToLibrary(
5064
storeListingVersionId,
5165
);
5266
completeStep("MARKETPLACE_ADD_AGENT");
5367
router.push(`/library/agents/${newLibraryAgent.id}`);
68+
toast({
69+
title: "Agent Added",
70+
description: "Redirecting to your library...",
71+
duration: 2000,
72+
});
5473
} catch (error) {
5574
console.error("Failed to add agent to library:", error);
75+
toast({
76+
title: "Error",
77+
description: "Failed to add agent to library. Please try again.",
78+
variant: "destructive",
79+
});
5680
}
57-
};
81+
}, [toast, api, storeListingVersionId, completeStep, router]);
5882

59-
const handleDownloadToLibrary = async () => {
83+
const handleDownload = useCallback(async () => {
6084
const downloadAgent = async (): Promise<void> => {
6185
setDownloading(true);
6286
try {
@@ -89,12 +113,16 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
89113
});
90114
} catch (error) {
91115
console.error(`Error downloading agent:`, error);
92-
throw error;
116+
toast({
117+
title: "Error",
118+
description: "Failed to download agent. Please try again.",
119+
variant: "destructive",
120+
});
93121
}
94122
};
95123
await downloadAgent();
96124
setDownloading(false);
97-
};
125+
}, [setDownloading, api, storeListingVersionId, toast]);
98126

99127
return (
100128
<div className="w-full max-w-[396px] px-4 sm:px-6 lg:w-[396px] lg:px-0">
@@ -105,65 +133,61 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
105133

106134
{/* Creator */}
107135
<div className="mb-3 flex w-full items-center gap-1.5 lg:mb-4">
108-
<div className="font-geist text-base font-normal text-neutral-800 dark:text-neutral-200 sm:text-lg lg:text-xl">
136+
<div className="font-sans text-base font-normal text-neutral-800 dark:text-neutral-200 sm:text-lg lg:text-xl">
109137
by
110138
</div>
111139
<Link
112140
href={`/marketplace/creator/${encodeURIComponent(creator)}`}
113-
className="font-geist text-base font-medium text-neutral-800 hover:underline dark:text-neutral-200 sm:text-lg lg:text-xl"
141+
className="font-sans text-base font-medium text-neutral-800 hover:underline dark:text-neutral-200 sm:text-lg lg:text-xl"
114142
>
115143
{creator}
116144
</Link>
117145
</div>
118146

119147
{/* Short Description */}
120-
<div className="font-geist mb-4 line-clamp-2 w-full text-base font-normal leading-normal text-neutral-600 dark:text-neutral-300 sm:text-lg lg:mb-6 lg:text-xl lg:leading-7">
148+
<div className="mb-4 line-clamp-2 w-full font-sans text-base font-normal leading-normal text-neutral-600 dark:text-neutral-300 sm:text-lg lg:mb-6 lg:text-xl lg:leading-7">
121149
{shortDescription}
122150
</div>
123151

124-
{/* Run Agent Button */}
125-
<div className="mb-4 w-full lg:mb-[60px]">
126-
{user ? (
152+
{/* Buttons */}
153+
<div className="mb-4 flex w-full gap-3 lg:mb-[60px]">
154+
{user && (
127155
<button
128-
onClick={handleAddToLibrary}
129-
className="inline-flex w-full items-center justify-center gap-2 rounded-[38px] bg-violet-600 px-4 py-3 transition-colors hover:bg-violet-700 sm:w-auto sm:gap-2.5 sm:px-5 sm:py-3.5 lg:px-6 lg:py-4"
130-
>
131-
<IconPlay className="h-5 w-5 text-white sm:h-5 sm:w-5 lg:h-6 lg:w-6" />
132-
<span className="font-poppins text-base font-medium text-neutral-50 sm:text-lg">
133-
Add To Library
134-
</span>
135-
</button>
136-
) : (
137-
<button
138-
onClick={handleDownloadToLibrary}
139-
className={`inline-flex w-full items-center justify-center gap-2 rounded-[38px] px-4 py-3 transition-colors sm:w-auto sm:gap-2.5 sm:px-5 sm:py-3.5 lg:px-6 lg:py-4 ${
140-
downloading
141-
? "bg-neutral-400"
142-
: "bg-violet-600 hover:bg-violet-700"
143-
}`}
144-
disabled={downloading}
145-
>
146-
{downloading ? (
147-
<LoaderIcon className="h-5 w-5 animate-spin text-white sm:h-5 sm:w-5 lg:h-6 lg:w-6" />
148-
) : (
149-
<DownloadIcon className="h-5 w-5 text-white sm:h-5 sm:w-5 lg:h-6 lg:w-6" />
156+
className={cn(
157+
"inline-flex min-w-24 items-center justify-center rounded-full bg-violet-600 px-4 py-3",
158+
"transition-colors duration-200 hover:bg-violet-500 disabled:bg-zinc-400",
150159
)}
151-
<span className="font-poppins text-base font-medium text-neutral-50 sm:text-lg">
152-
{downloading ? "Downloading..." : "Download Agent as File"}
160+
onClick={libraryAction}
161+
disabled={adding}
162+
>
163+
<span className="justify-start font-sans text-sm font-medium leading-snug text-primary-foreground">
164+
{libraryAgent ? "See runs" : "Add to library"}
153165
</span>
154166
</button>
155167
)}
168+
<button
169+
className={cn(
170+
"inline-flex min-w-24 items-center justify-center rounded-full bg-zinc-200 px-4 py-3",
171+
"transition-colors duration-200 hover:bg-zinc-200/70 disabled:bg-zinc-200/40",
172+
)}
173+
onClick={handleDownload}
174+
disabled={downloading}
175+
>
176+
<div className="justify-start text-center font-sans text-sm font-medium leading-snug text-zinc-800">
177+
Download agent
178+
</div>
179+
</button>
156180
</div>
157181

158182
{/* Rating and Runs */}
159183
<div className="mb-4 flex w-full items-center justify-between lg:mb-[44px]">
160184
<div className="flex items-center gap-1.5 sm:gap-2">
161-
<span className="font-geist whitespace-nowrap text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
185+
<span className="whitespace-nowrap font-sans text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
162186
{rating.toFixed(1)}
163187
</span>
164188
<div className="flex gap-0.5">{StarRatingIcons(rating)}</div>
165189
</div>
166-
<div className="font-geist whitespace-nowrap text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
190+
<div className="whitespace-nowrap font-sans text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
167191
{runs.toLocaleString()} runs
168192
</div>
169193
</div>
@@ -183,14 +207,14 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
183207

184208
{/* Categories */}
185209
<div className="mb-4 flex w-full flex-col gap-1.5 sm:gap-2 lg:mb-[36px]">
186-
<div className="font-geist decoration-skip-ink-none mb-1.5 text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2">
210+
<div className="decoration-skip-ink-none mb-1.5 font-sans text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2">
187211
Categories
188212
</div>
189213
<div className="flex flex-wrap gap-1.5 sm:gap-2">
190214
{categories.map((category, index) => (
191215
<div
192216
key={index}
193-
className="font-geist decoration-skip-ink-none whitespace-nowrap rounded-full border border-neutral-600 bg-white px-2 py-0.5 text-base font-normal leading-6 text-neutral-800 underline-offset-[from-font] dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-200 sm:px-[16px] sm:py-[10px]"
217+
className="decoration-skip-ink-none whitespace-nowrap rounded-full border border-neutral-600 bg-white px-2 py-0.5 font-sans text-base font-normal leading-6 text-neutral-800 underline-offset-[from-font] dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-200 sm:px-[16px] sm:py-[10px]"
194218
>
195219
{category}
196220
</div>
@@ -200,10 +224,10 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
200224

201225
{/* Version History */}
202226
<div className="flex w-full flex-col gap-0.5 sm:gap-1">
203-
<div className="font-geist decoration-skip-ink-none mb-1.5 text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2">
227+
<div className="decoration-skip-ink-none mb-1.5 font-sans text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2">
204228
Version history
205229
</div>
206-
<div className="font-geist decoration-skip-ink-none text-base font-normal leading-6 text-neutral-600 underline-offset-[from-font] dark:text-neutral-400">
230+
<div className="decoration-skip-ink-none font-sans text-base font-normal leading-6 text-neutral-600 underline-offset-[from-font] dark:text-neutral-400">
207231
Last updated {lastUpdated}
208232
</div>
209233
<div className="text-xs text-neutral-600 dark:text-neutral-400 sm:text-sm">

0 commit comments

Comments
 (0)