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 <zamil.majdy@agpt.co>
This commit is contained in:
Krzysztof Czerwinski
2025-05-05 18:47:58 +02:00
committed by GitHub
parent 79319ad1a7
commit 6f1578239a
6 changed files with 159 additions and 59 deletions

View File

@@ -175,6 +175,44 @@ async def get_library_agent(id: str, user_id: str) -> library_model.LibraryAgent
raise store_exceptions.DatabaseError("Failed to fetch library agent") from e
async def get_library_agent_by_store_version_id(
store_listing_version_id: str,
user_id: str,
):
"""
Get the library agent metadata for a given store listing version ID and user ID.
"""
logger.debug(
f"Getting library agent for store listing ID: {store_listing_version_id}"
)
store_listing_version = (
await prisma.models.StoreListingVersion.prisma().find_unique(
where={"id": store_listing_version_id},
)
)
if not store_listing_version:
logger.warning(f"Store listing version not found: {store_listing_version_id}")
raise store_exceptions.AgentNotFoundError(
f"Store listing version {store_listing_version_id} not found or invalid"
)
# Check if user already has this agent
agent = await prisma.models.LibraryAgent.prisma().find_first(
where={
"userId": user_id,
"agentGraphId": store_listing_version.agentGraphId,
"agentGraphVersion": store_listing_version.agentGraphVersion,
"isDeleted": False,
},
include={"AgentGraph": True},
)
if agent:
return library_model.LibraryAgent.from_db(agent)
else:
return None
async def add_generated_agent_image(
graph: backend.data.graph.GraphModel,
library_agent_id: str,
@@ -397,11 +435,6 @@ async def add_store_agent_to_library(
)
graph = store_listing_version.AgentGraph
if graph.userId == user_id:
logger.warning(
f"User #{user_id} attempted to add their own agent to their library"
)
raise store_exceptions.DatabaseError("Cannot add own agent to library")
# Check if user already has this agent
existing_library_agent = (
@@ -411,7 +444,7 @@ async def add_store_agent_to_library(
"agentGraphId": graph.id,
"agentGraphVersion": graph.version,
},
include=library_agent_include(user_id),
include={"AgentGraph": True},
)
)
if existing_library_agent:

View File

@@ -85,6 +85,30 @@ async def get_library_agent(
return await library_db.get_library_agent(id=library_agent_id, user_id=user_id)
@router.get(
"/marketplace/{store_listing_version_id}/",
tags=["store, library"],
response_model=library_model.LibraryAgent | None,
)
async def get_library_agent_by_store_listing_version_id(
store_listing_version_id: str,
user_id: str = Depends(autogpt_auth_lib.depends.get_user_id),
):
"""
Get Library Agent from Store Listing Version ID.
"""
try:
return await library_db.get_library_agent_by_store_version_id(
store_listing_version_id, user_id
)
except Exception as e:
logger.error(f"Could not fetch library agent from store version ID: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to add agent to library",
) from e
@router.post(
"",
status_code=status.HTTP_201_CREATED,

View File

@@ -6,6 +6,7 @@ import { AgentsSection } from "@/components/agptui/composite/AgentsSection";
import { BecomeACreator } from "@/components/agptui/BecomeACreator";
import { Separator } from "@/components/ui/separator";
import { Metadata } from "next";
import getServerSupabase from "@/lib/supabase/getServerSupabase";
export async function generateMetadata({
params,
@@ -16,7 +17,7 @@ export async function generateMetadata({
const agent = await api.getStoreAgent(params.creator, params.slug);
return {
title: `${agent.agent_name} - AutoGPT Store`,
title: `${agent.agent_name} - AutoGPT Marketplace`,
description: agent.description,
};
}
@@ -43,9 +44,17 @@ export default async function Page({
// We are using slug as we know its has been sanitized and is not null
search_query: agent.slug.replace(/-/g, " "),
});
const {
data: { user },
} = await getServerSupabase().auth.getUser();
const libraryAgent = user
? await api.getLibraryAgentByStoreListingVersionID(
agent.store_listing_version_id,
)
: null;
const breadcrumbs = [
{ name: "Store", link: "/marketplace" },
{ name: "Marketplace", link: "/marketplace" },
{
name: agent.creator,
link: `/marketplace/creator/${encodeURIComponent(agent.creator)}`,
@@ -61,6 +70,7 @@ export default async function Page({
<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">
<div className="w-full md:w-auto md:shrink-0">
<AgentInfo
user={user}
name={agent.agent_name}
creator={agent.creator}
shortDescription={agent.sub_heading}
@@ -71,6 +81,7 @@ export default async function Page({
lastUpdated={agent.updated_at}
version={agent.versions[agent.versions.length - 1]}
storeListingVersionId={agent.store_listing_version_id}
libraryAgent={libraryAgent}
/>
</div>
<AgentImages

View File

@@ -27,6 +27,8 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
user: null,
libraryAgent: null,
name: "AI Video Generator",
storeListingVersionId: "123",
creator: "Toran Richards",

View File

@@ -1,17 +1,19 @@
"use client";
import * as React from "react";
import { IconPlay, StarRatingIcons } from "@/components/ui/icons";
import { StarRatingIcons } from "@/components/ui/icons";
import { Separator } from "@/components/ui/separator";
import BackendAPI from "@/lib/autogpt-server-api";
import BackendAPI, { LibraryAgent } from "@/lib/autogpt-server-api";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { useToast } from "@/components/ui/use-toast";
import useSupabase from "@/hooks/useSupabase";
import { DownloadIcon, LoaderIcon } from "lucide-react";
import { useOnboarding } from "../onboarding/onboarding-provider";
import { User } from "@supabase/supabase-js";
import { cn } from "@/lib/utils";
import { FC, useCallback, useMemo, useState } from "react";
interface AgentInfoProps {
user: User | null;
name: string;
creator: string;
shortDescription: string;
@@ -22,9 +24,11 @@ interface AgentInfoProps {
lastUpdated: string;
version: string;
storeListingVersionId: string;
libraryAgent: LibraryAgent | null;
}
export const AgentInfo: React.FC<AgentInfoProps> = ({
export const AgentInfo: FC<AgentInfoProps> = ({
user,
name,
creator,
shortDescription,
@@ -35,28 +39,48 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
lastUpdated,
version,
storeListingVersionId,
libraryAgent,
}) => {
const router = useRouter();
const api = React.useMemo(() => new BackendAPI(), []);
const { user } = useSupabase();
const api = useMemo(() => new BackendAPI(), []);
const { toast } = useToast();
const { completeStep } = useOnboarding();
const [adding, setAdding] = useState(false);
const [downloading, setDownloading] = useState(false);
const [downloading, setDownloading] = React.useState(false);
const handleAddToLibrary = async () => {
const libraryAction = useCallback(async () => {
setAdding(true);
if (libraryAgent) {
toast({
description: "Redirecting to your library...",
duration: 2000,
});
// Redirect to the library agent page
router.push(`/library/agents/${libraryAgent.id}`);
return;
}
try {
const newLibraryAgent = await api.addMarketplaceAgentToLibrary(
storeListingVersionId,
);
completeStep("MARKETPLACE_ADD_AGENT");
router.push(`/library/agents/${newLibraryAgent.id}`);
toast({
title: "Agent Added",
description: "Redirecting to your library...",
duration: 2000,
});
} catch (error) {
console.error("Failed to add agent to library:", error);
toast({
title: "Error",
description: "Failed to add agent to library. Please try again.",
variant: "destructive",
});
}
};
}, [toast, api, storeListingVersionId, completeStep, router]);
const handleDownloadToLibrary = async () => {
const handleDownload = useCallback(async () => {
const downloadAgent = async (): Promise<void> => {
setDownloading(true);
try {
@@ -89,12 +113,16 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
});
} catch (error) {
console.error(`Error downloading agent:`, error);
throw error;
toast({
title: "Error",
description: "Failed to download agent. Please try again.",
variant: "destructive",
});
}
};
await downloadAgent();
setDownloading(false);
};
}, [setDownloading, api, storeListingVersionId, toast]);
return (
<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> = ({
{/* Creator */}
<div className="mb-3 flex w-full items-center gap-1.5 lg:mb-4">
<div className="font-geist text-base font-normal text-neutral-800 dark:text-neutral-200 sm:text-lg lg:text-xl">
<div className="font-sans text-base font-normal text-neutral-800 dark:text-neutral-200 sm:text-lg lg:text-xl">
by
</div>
<Link
href={`/marketplace/creator/${encodeURIComponent(creator)}`}
className="font-geist text-base font-medium text-neutral-800 hover:underline dark:text-neutral-200 sm:text-lg lg:text-xl"
className="font-sans text-base font-medium text-neutral-800 hover:underline dark:text-neutral-200 sm:text-lg lg:text-xl"
>
{creator}
</Link>
</div>
{/* Short Description */}
<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">
<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">
{shortDescription}
</div>
{/* Run Agent Button */}
<div className="mb-4 w-full lg:mb-[60px]">
{user ? (
{/* Buttons */}
<div className="mb-4 flex w-full gap-3 lg:mb-[60px]">
{user && (
<button
onClick={handleAddToLibrary}
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"
>
<IconPlay className="h-5 w-5 text-white sm:h-5 sm:w-5 lg:h-6 lg:w-6" />
<span className="font-poppins text-base font-medium text-neutral-50 sm:text-lg">
Add To Library
</span>
</button>
) : (
<button
onClick={handleDownloadToLibrary}
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 ${
downloading
? "bg-neutral-400"
: "bg-violet-600 hover:bg-violet-700"
}`}
disabled={downloading}
>
{downloading ? (
<LoaderIcon className="h-5 w-5 animate-spin text-white sm:h-5 sm:w-5 lg:h-6 lg:w-6" />
) : (
<DownloadIcon className="h-5 w-5 text-white sm:h-5 sm:w-5 lg:h-6 lg:w-6" />
className={cn(
"inline-flex min-w-24 items-center justify-center rounded-full bg-violet-600 px-4 py-3",
"transition-colors duration-200 hover:bg-violet-500 disabled:bg-zinc-400",
)}
<span className="font-poppins text-base font-medium text-neutral-50 sm:text-lg">
{downloading ? "Downloading..." : "Download Agent as File"}
onClick={libraryAction}
disabled={adding}
>
<span className="justify-start font-sans text-sm font-medium leading-snug text-primary-foreground">
{libraryAgent ? "See runs" : "Add to library"}
</span>
</button>
)}
<button
className={cn(
"inline-flex min-w-24 items-center justify-center rounded-full bg-zinc-200 px-4 py-3",
"transition-colors duration-200 hover:bg-zinc-200/70 disabled:bg-zinc-200/40",
)}
onClick={handleDownload}
disabled={downloading}
>
<div className="justify-start text-center font-sans text-sm font-medium leading-snug text-zinc-800">
Download agent
</div>
</button>
</div>
{/* Rating and Runs */}
<div className="mb-4 flex w-full items-center justify-between lg:mb-[44px]">
<div className="flex items-center gap-1.5 sm:gap-2">
<span className="font-geist whitespace-nowrap text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
<span className="whitespace-nowrap font-sans text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
{rating.toFixed(1)}
</span>
<div className="flex gap-0.5">{StarRatingIcons(rating)}</div>
</div>
<div className="font-geist whitespace-nowrap text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
<div className="whitespace-nowrap font-sans text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
{runs.toLocaleString()} runs
</div>
</div>
@@ -183,14 +207,14 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
{/* Categories */}
<div className="mb-4 flex w-full flex-col gap-1.5 sm:gap-2 lg:mb-[36px]">
<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">
<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">
Categories
</div>
<div className="flex flex-wrap gap-1.5 sm:gap-2">
{categories.map((category, index) => (
<div
key={index}
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]"
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]"
>
{category}
</div>
@@ -200,10 +224,10 @@ export const AgentInfo: React.FC<AgentInfoProps> = ({
{/* Version History */}
<div className="flex w-full flex-col gap-0.5 sm:gap-1">
<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">
<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">
Version history
</div>
<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">
<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">
Last updated {lastUpdated}
</div>
<div className="text-xs text-neutral-600 dark:text-neutral-400 sm:text-sm">

View File

@@ -608,6 +608,12 @@ export default class BackendAPI {
return this._get(`/library/agents/${id}`);
}
getLibraryAgentByStoreListingVersionID(
storeListingVersionId: string,
): Promise<LibraryAgent | null> {
return this._get(`/library/agents/marketplace/${storeListingVersionId}`);
}
addMarketplaceAgentToLibrary(
storeListingVersionID: string,
): Promise<LibraryAgent> {