mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 15:17:59 -05:00
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:
committed by
GitHub
parent
79319ad1a7
commit
6f1578239a
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user