mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
refactor(frontend): Revamp marketplace agent page data fetching and structure (#10756)
- Updated the agent page to utilize React Query for data fetching,
improving performance and reliability.
- Removed legacy API calls and integrated prefetching for creator
details and agents.
- Introduced a new MainAgentPage component for better separation of
concerns.
- Added a hydration boundary for managing server state.
> It’s important to note that I haven’t changed any UI in this, as it’s
out of scope for this PR.
### Checklist 📋
- [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] I have manually tested both `Add to Library` and `Download`
functions, and they are working correctly.
- [x] All fetching functions are working perfectly.
- [x] All end-to-end tests are also working correctly.
This commit is contained in:
@@ -1,124 +1,75 @@
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
|
||||
import { AgentInfo } from "@/components/agptui/AgentInfo";
|
||||
import { AgentImages } from "@/components/agptui/AgentImages";
|
||||
import { AgentsSection } from "@/components/agptui/composite/AgentsSection";
|
||||
import { BecomeACreator } from "@/components/agptui/BecomeACreator";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Metadata } from "next";
|
||||
import { getServerUser } from "@/lib/supabase/server/getServerUser";
|
||||
import {
|
||||
getV2GetSpecificAgent,
|
||||
prefetchGetV2GetSpecificAgentQuery,
|
||||
prefetchGetV2ListStoreAgentsQuery,
|
||||
} from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
|
||||
import { MainAgentPage } from "../../../components/MainAgentPage/MainAgentPage";
|
||||
import { getQueryClient } from "@/lib/react-query/queryClient";
|
||||
import { prefetchGetV2GetAgentByStoreIdQuery } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
|
||||
|
||||
// Force dynamic rendering to avoid static generation issues with cookies
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type MarketplaceAgentPageParams = { creator: string; slug: string };
|
||||
export type MarketplaceAgentPageParams = { creator: string; slug: string };
|
||||
|
||||
export async function generateMetadata({
|
||||
params: _params,
|
||||
}: {
|
||||
params: Promise<MarketplaceAgentPageParams>;
|
||||
}): Promise<Metadata> {
|
||||
const api = new BackendAPI();
|
||||
const params = await _params;
|
||||
const agent = await api.getStoreAgent(params.creator, params.slug);
|
||||
|
||||
const { data: creator_agent } = await getV2GetSpecificAgent(
|
||||
params.creator,
|
||||
params.slug,
|
||||
);
|
||||
return {
|
||||
title: `${agent.agent_name} - AutoGPT Marketplace`,
|
||||
description: agent.description,
|
||||
title: `${(creator_agent as StoreAgentDetails).agent_name} - AutoGPT Marketplace`,
|
||||
description: (creator_agent as StoreAgentDetails).description,
|
||||
};
|
||||
}
|
||||
|
||||
// export async function generateStaticParams() {
|
||||
// const api = new BackendAPI();
|
||||
// const agents = await api.getStoreAgents({ featured: true });
|
||||
// return agents.agents.map((agent) => ({
|
||||
// creator: agent.creator,
|
||||
// slug: agent.slug,
|
||||
// }));
|
||||
// }
|
||||
|
||||
export default async function MarketplaceAgentPage({
|
||||
params: _params,
|
||||
}: {
|
||||
params: Promise<MarketplaceAgentPageParams>;
|
||||
}) {
|
||||
const queryClient = getQueryClient();
|
||||
|
||||
const params = await _params;
|
||||
const creator_lower = params.creator.toLowerCase();
|
||||
const { user } = await getServerUser();
|
||||
const api = new BackendAPI();
|
||||
const agent = await api.getStoreAgent(creator_lower, params.slug);
|
||||
const otherAgents = await api.getStoreAgents({ creator: creator_lower });
|
||||
const similarAgents = await api.getStoreAgents({
|
||||
// We are using slug as we know its has been sanitized and is not null
|
||||
search_query: agent.slug.replace(/-/g, " "),
|
||||
});
|
||||
const libraryAgent = user
|
||||
? await api
|
||||
.getLibraryAgentByStoreListingVersionID(agent.active_version_id || "")
|
||||
.catch((error) => {
|
||||
console.error("Failed to fetch library agent:", error);
|
||||
return null;
|
||||
})
|
||||
: null;
|
||||
await Promise.all([
|
||||
prefetchGetV2GetSpecificAgentQuery(queryClient, creator_lower, params.slug),
|
||||
prefetchGetV2ListStoreAgentsQuery(queryClient, {
|
||||
creator: creator_lower,
|
||||
}),
|
||||
prefetchGetV2ListStoreAgentsQuery(queryClient, {
|
||||
search_query: params.slug.replace(/-/g, " "),
|
||||
}),
|
||||
]);
|
||||
|
||||
const breadcrumbs = [
|
||||
{ name: "Marketplace", link: "/marketplace" },
|
||||
{
|
||||
name: agent.creator,
|
||||
link: `/marketplace/creator/${encodeURIComponent(agent.creator)}`,
|
||||
},
|
||||
{ name: agent.agent_name, link: "#" },
|
||||
];
|
||||
const { user } = await getServerUser();
|
||||
const { data: creator_agent, status } = await getV2GetSpecificAgent(
|
||||
creator_lower,
|
||||
params.slug,
|
||||
); // Already cached in above prefetch
|
||||
if (status === 200) {
|
||||
await prefetchGetV2GetAgentByStoreIdQuery(
|
||||
queryClient,
|
||||
creator_agent.active_version_id ?? "",
|
||||
{
|
||||
query: {
|
||||
enabled: !!user && !!creator_agent.active_version_id,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-screen max-w-[1360px]">
|
||||
<main className="mt-5 px-4">
|
||||
<Breadcrumbs items={breadcrumbs} />
|
||||
|
||||
<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}
|
||||
longDescription={agent.description}
|
||||
rating={agent.rating}
|
||||
runs={agent.runs}
|
||||
categories={agent.categories}
|
||||
lastUpdated={agent.updated_at}
|
||||
version={agent.versions[agent.versions.length - 1]}
|
||||
storeListingVersionId={agent.store_listing_version_id}
|
||||
libraryAgent={libraryAgent}
|
||||
/>
|
||||
</div>
|
||||
<AgentImages
|
||||
images={
|
||||
agent.agent_video
|
||||
? [agent.agent_video, ...agent.agent_image]
|
||||
: agent.agent_image
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
<AgentsSection
|
||||
margin="32px"
|
||||
agents={otherAgents.agents}
|
||||
sectionTitle={`Other agents by ${agent.creator}`}
|
||||
/>
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
<AgentsSection
|
||||
margin="32px"
|
||||
agents={similarAgents.agents}
|
||||
sectionTitle="Similar agents"
|
||||
/>
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
<BecomeACreator
|
||||
title="Become a Creator"
|
||||
description="Join our ever-growing community of hackers and tinkerers"
|
||||
buttonText="Become a Creator"
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||
<MainAgentPage params={params} />
|
||||
</HydrationBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import Image from "next/image";
|
||||
import { PlayIcon } from "@radix-ui/react-icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
getYouTubeVideoId,
|
||||
isValidVideoFile,
|
||||
isValidVideoUrl,
|
||||
} from "./helpers";
|
||||
import { useAgentImageItem } from "./useAgentImageItem";
|
||||
|
||||
interface AgentImageItemProps {
|
||||
image: string;
|
||||
index: number;
|
||||
playingVideoIndex: number | null;
|
||||
handlePlay: (index: number) => void;
|
||||
handlePause: (index: number) => void;
|
||||
}
|
||||
|
||||
export const AgentImageItem: React.FC<AgentImageItemProps> = ({
|
||||
image,
|
||||
index,
|
||||
playingVideoIndex,
|
||||
handlePlay,
|
||||
handlePause,
|
||||
}) => {
|
||||
const { videoRef } = useAgentImageItem({ playingVideoIndex, index });
|
||||
const isVideoFile = isValidVideoFile(image);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="h-[15rem] overflow-hidden rounded-[26px] bg-[#a8a8a8] dark:bg-neutral-700 sm:h-[20rem] sm:w-full md:h-[25rem] lg:h-[30rem]">
|
||||
{isValidVideoUrl(image) ? (
|
||||
getYouTubeVideoId(image) ? (
|
||||
<iframe
|
||||
width="100%"
|
||||
height="100%"
|
||||
src={`https://www.youtube.com/embed/${getYouTubeVideoId(image)}`}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
title="YouTube video player"
|
||||
></iframe>
|
||||
) : (
|
||||
<div className="relative h-full w-full overflow-hidden">
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
controls
|
||||
preload="metadata"
|
||||
poster={`${image}#t=0.1`}
|
||||
style={{ objectPosition: "center 25%" }}
|
||||
onPlay={() => handlePlay(index)}
|
||||
onPause={() => handlePause(index)}
|
||||
autoPlay={false}
|
||||
title="Video"
|
||||
>
|
||||
<source src={image} type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="relative h-full w-full">
|
||||
<Image
|
||||
src={image}
|
||||
alt="Image"
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
className="rounded-xl object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isVideoFile && playingVideoIndex !== index && (
|
||||
<div className="absolute bottom-2 left-2 sm:bottom-3 sm:left-3 md:bottom-4 md:left-4 lg:bottom-[1.25rem] lg:left-[1.25rem]">
|
||||
<Button
|
||||
size="default"
|
||||
onClick={() => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.play();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="pr-1 text-sm font-medium leading-6 tracking-tight text-[#272727] dark:text-neutral-200 sm:pr-2 sm:text-base sm:leading-7 md:text-lg md:leading-8 lg:text-xl lg:leading-9">
|
||||
Play demo
|
||||
</span>
|
||||
<PlayIcon className="h-5 w-5 text-black dark:text-neutral-200 sm:h-6 sm:w-6 md:h-7 md:w-7" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
export const isValidVideoFile = (url: string): boolean => {
|
||||
const videoExtensions = /\.(mp4|webm|ogg)$/i;
|
||||
return videoExtensions.test(url);
|
||||
};
|
||||
|
||||
export const isValidVideoUrl = (url: string): boolean => {
|
||||
const videoExtensions = /\.(mp4|webm|ogg)$/i;
|
||||
const youtubeRegex = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/;
|
||||
return videoExtensions.test(url) || youtubeRegex.test(url);
|
||||
};
|
||||
|
||||
export const getYouTubeVideoId = (url: string) => {
|
||||
const regExp =
|
||||
/^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
|
||||
const match = url.match(regExp);
|
||||
return match && match[7].length === 11 ? match[7] : null;
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
interface UseAgentImageItemProps {
|
||||
playingVideoIndex: number | null;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export const useAgentImageItem = ({
|
||||
playingVideoIndex,
|
||||
index,
|
||||
}: UseAgentImageItemProps) => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
playingVideoIndex !== index &&
|
||||
videoRef.current &&
|
||||
!videoRef.current.paused
|
||||
) {
|
||||
videoRef.current.pause();
|
||||
}
|
||||
}, [playingVideoIndex, index]);
|
||||
|
||||
return {
|
||||
videoRef,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
import { AgentImageItem } from "../AgentImageItem/AgentImageItem";
|
||||
import { useAgentImage } from "./useAgentImage";
|
||||
|
||||
interface AgentImagesProps {
|
||||
images: string[];
|
||||
}
|
||||
|
||||
export const AgentImages: React.FC<AgentImagesProps> = ({ images }) => {
|
||||
const { playingVideoIndex, handlePlay, handlePause } = useAgentImage();
|
||||
return (
|
||||
<div className="w-full overflow-y-auto bg-white px-2 dark:bg-transparent lg:w-[56.25rem]">
|
||||
<div className="space-y-4 sm:space-y-6 md:space-y-[1.875rem]">
|
||||
{images.map((image, index) => (
|
||||
<AgentImageItem
|
||||
key={index}
|
||||
image={image}
|
||||
index={index}
|
||||
playingVideoIndex={playingVideoIndex}
|
||||
handlePlay={handlePlay}
|
||||
handlePause={handlePause}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export const useAgentImage = () => {
|
||||
const [playingVideoIndex, setPlayingVideoIndex] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const handlePlay = useCallback((index: number) => {
|
||||
setPlayingVideoIndex(index);
|
||||
}, []);
|
||||
|
||||
const handlePause = useCallback(
|
||||
(index: number) => {
|
||||
if (playingVideoIndex === index) {
|
||||
setPlayingVideoIndex(null);
|
||||
}
|
||||
},
|
||||
[playingVideoIndex],
|
||||
);
|
||||
|
||||
return {
|
||||
playingVideoIndex,
|
||||
handlePlay,
|
||||
handlePause,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,168 @@
|
||||
"use client";
|
||||
|
||||
import { StarRatingIcons } from "@/components/ui/icons";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import Link from "next/link";
|
||||
import { User } from "@supabase/supabase-js";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAgentInfo } from "./useAgentInfo";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
|
||||
interface AgentInfoProps {
|
||||
user: User | null;
|
||||
name: string;
|
||||
creator: string;
|
||||
shortDescription: string;
|
||||
longDescription: string;
|
||||
rating: number;
|
||||
runs: number;
|
||||
categories: string[];
|
||||
lastUpdated: string;
|
||||
version: string;
|
||||
storeListingVersionId: string;
|
||||
libraryAgent: LibraryAgent | undefined;
|
||||
}
|
||||
|
||||
export const AgentInfo = ({
|
||||
user,
|
||||
name,
|
||||
creator,
|
||||
shortDescription,
|
||||
longDescription,
|
||||
rating,
|
||||
runs,
|
||||
categories,
|
||||
lastUpdated,
|
||||
version,
|
||||
storeListingVersionId,
|
||||
libraryAgent,
|
||||
}: AgentInfoProps) => {
|
||||
const {
|
||||
handleDownload,
|
||||
isDownloadingAgent,
|
||||
handleLibraryAction,
|
||||
isAddingAgentToLibrary,
|
||||
} = useAgentInfo({ storeListingVersionId });
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[396px] px-4 sm:px-6 lg:w-[396px] lg:px-0">
|
||||
{/* Title */}
|
||||
<div
|
||||
data-testid="agent-title"
|
||||
className="mb-3 w-full font-poppins text-2xl font-medium leading-normal text-neutral-900 dark:text-neutral-100 sm:text-3xl lg:mb-4 lg:text-[35px] lg:leading-10"
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
{/* Creator */}
|
||||
<div className="mb-3 flex w-full items-center gap-1.5 lg:mb-4">
|
||||
<div className="text-base font-normal text-neutral-800 dark:text-neutral-200 sm:text-lg lg:text-xl">
|
||||
by
|
||||
</div>
|
||||
<Link
|
||||
data-testid={"agent-creator"}
|
||||
href={`/marketplace/creator/${encodeURIComponent(creator)}`}
|
||||
className="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="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">
|
||||
{shortDescription}
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="mb-4 flex w-full gap-3 lg:mb-[60px]">
|
||||
{user && (
|
||||
<button
|
||||
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",
|
||||
)}
|
||||
data-testid={"agent-add-library-button"}
|
||||
onClick={handleLibraryAction}
|
||||
disabled={isAddingAgentToLibrary}
|
||||
>
|
||||
<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",
|
||||
)}
|
||||
data-testid={"agent-download-button"}
|
||||
onClick={handleDownload}
|
||||
disabled={isDownloadingAgent}
|
||||
>
|
||||
<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="whitespace-nowrap 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="whitespace-nowrap text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
|
||||
{runs.toLocaleString()} runs
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<Separator className="mb-4 lg:mb-[44px]" />
|
||||
|
||||
{/* Description Section */}
|
||||
<div className="mb-4 w-full lg:mb-[36px]">
|
||||
<div className="decoration-skip-ink-none mb-1.5 text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2">
|
||||
Description
|
||||
</div>
|
||||
<div
|
||||
data-testid={"agent-description"}
|
||||
className="whitespace-pre-line text-base font-normal leading-6 text-neutral-600 dark:text-neutral-400"
|
||||
>
|
||||
{longDescription}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="mb-4 flex w-full flex-col gap-1.5 sm:gap-2 lg:mb-[36px]">
|
||||
<div className="decoration-skip-ink-none mb-1.5 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="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]"
|
||||
>
|
||||
{category}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Version History */}
|
||||
<div className="flex w-full flex-col gap-0.5 sm:gap-1">
|
||||
<div className="decoration-skip-ink-none mb-1.5 text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2">
|
||||
Version history
|
||||
</div>
|
||||
<div className="decoration-skip-ink-none 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">
|
||||
Version {version}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import { usePostV2AddMarketplaceAgent } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
|
||||
import { useGetV2DownloadAgentFile } from "@/app/api/__generated__/endpoints/store/store";
|
||||
|
||||
interface UseAgentInfoProps {
|
||||
storeListingVersionId: string;
|
||||
}
|
||||
|
||||
export const useAgentInfo = ({ storeListingVersionId }: UseAgentInfoProps) => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { completeStep } = useOnboarding();
|
||||
|
||||
const {
|
||||
mutate: addMarketplaceAgentToLibrary,
|
||||
isPending: isAddingAgentToLibrary,
|
||||
} = usePostV2AddMarketplaceAgent({
|
||||
mutation: {
|
||||
onSuccess: ({ data }) => {
|
||||
completeStep("MARKETPLACE_ADD_AGENT");
|
||||
router.push(`/library/agents/${(data as LibraryAgent).id}`);
|
||||
toast({
|
||||
title: "Agent Added",
|
||||
description: "Redirecting to your library...",
|
||||
duration: 2000,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
Sentry.captureException(error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to add agent to library. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { refetch: downloadAgent, isFetching: isDownloadingAgent } =
|
||||
useGetV2DownloadAgentFile(storeListingVersionId, {
|
||||
query: {
|
||||
enabled: false,
|
||||
select: (data) => {
|
||||
return data.data;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const handleLibraryAction = async () => {
|
||||
addMarketplaceAgentToLibrary({
|
||||
data: { store_listing_version_id: storeListingVersionId },
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const { data: file } = await downloadAgent();
|
||||
|
||||
const jsonData = JSON.stringify(file, null, 2);
|
||||
const blob = new Blob([jsonData], { type: "application/json" });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `agent_${storeListingVersionId}.json`;
|
||||
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
toast({
|
||||
title: "Download Complete",
|
||||
description: "Your agent has been successfully downloaded.",
|
||||
});
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to download agent. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isAddingAgentToLibrary,
|
||||
handleLibraryAction,
|
||||
handleDownload,
|
||||
isDownloadingAgent,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export const AgentPageLoading = () => {
|
||||
return (
|
||||
<div className="mx-auto w-screen max-w-[1360px]">
|
||||
<main className="mt-5 px-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<span>/</span>
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<span>/</span>
|
||||
<Skeleton className="h-4 w-40" />
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex flex-col gap-8 md:flex-row">
|
||||
<div className="w-full max-w-sm">
|
||||
<Skeleton className="h-64 w-full rounded-lg" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Skeleton className="aspect-video w-full rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
"use client";
|
||||
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
|
||||
import { useMainAgentPage } from "./useMainAgentPage";
|
||||
import { MarketplaceAgentPageParams } from "../../agent/[creator]/[slug]/page";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { AgentsSection } from "../AgentsSection/AgentsSection";
|
||||
import { BecomeACreator } from "../BecomeACreator/BecomeACreator";
|
||||
import { AgentPageLoading } from "../AgentPageLoading";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { AgentInfo } from "../AgentInfo/AgentInfo";
|
||||
import { AgentImages } from "../AgentImages/AgentImage";
|
||||
|
||||
type MainAgentPageProps = {
|
||||
params: MarketplaceAgentPageParams;
|
||||
};
|
||||
|
||||
export const MainAgentPage = ({ params }: MainAgentPageProps) => {
|
||||
const {
|
||||
agent,
|
||||
otherAgents,
|
||||
similarAgents,
|
||||
libraryAgent,
|
||||
isLoading,
|
||||
hasError,
|
||||
user,
|
||||
} = useMainAgentPage({ params });
|
||||
|
||||
if (isLoading) {
|
||||
return <AgentPageLoading />;
|
||||
}
|
||||
if (hasError) {
|
||||
return (
|
||||
<div className="mx-auto w-screen max-w-[1360px]">
|
||||
<main className="px-4">
|
||||
<div className="flex min-h-[400px] items-center justify-center">
|
||||
<ErrorCard
|
||||
isSuccess={false}
|
||||
responseError={{ message: "Failed to load agent data" }}
|
||||
context="agent page"
|
||||
onRetry={() => window.location.reload()}
|
||||
className="w-full max-w-md"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!agent) {
|
||||
return (
|
||||
<div className="mx-auto w-screen max-w-[1360px]">
|
||||
<main className="px-4">
|
||||
<div className="flex min-h-[400px] items-center justify-center">
|
||||
<ErrorCard
|
||||
isSuccess={false}
|
||||
responseError={{ message: "Agent not found" }}
|
||||
context="agent page"
|
||||
onRetry={() => window.location.reload()}
|
||||
className="w-full max-w-md"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const breadcrumbs = [
|
||||
{ name: "Markertplace", link: "/marketplace" },
|
||||
{
|
||||
name: agent.creator,
|
||||
link: `/marketplace/creator/${encodeURIComponent(agent.creator)}`,
|
||||
},
|
||||
{ name: agent.agent_name, link: "#" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-screen max-w-[1360px]">
|
||||
<main className="mt-5 px-4">
|
||||
<Breadcrumbs items={breadcrumbs} />
|
||||
|
||||
<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}
|
||||
longDescription={agent.description}
|
||||
rating={agent.rating}
|
||||
runs={agent.runs}
|
||||
categories={agent.categories}
|
||||
lastUpdated={agent.last_updated.toISOString()}
|
||||
version={agent.versions[agent.versions.length - 1]}
|
||||
storeListingVersionId={agent.store_listing_version_id}
|
||||
libraryAgent={libraryAgent}
|
||||
/>
|
||||
</div>
|
||||
<AgentImages
|
||||
images={
|
||||
agent.agent_video
|
||||
? [agent.agent_video, ...agent.agent_image]
|
||||
: agent.agent_image
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
{otherAgents && (
|
||||
<AgentsSection
|
||||
margin="32px"
|
||||
agents={otherAgents.agents}
|
||||
sectionTitle={`Other agents by ${agent.creator}`}
|
||||
/>
|
||||
)}
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
{similarAgents && (
|
||||
<AgentsSection
|
||||
margin="32px"
|
||||
agents={similarAgents.agents}
|
||||
sectionTitle="Similar agents"
|
||||
/>
|
||||
)}
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
<BecomeACreator
|
||||
title="Become a Creator"
|
||||
description="Join our ever-growing community of hackers and tinkerers"
|
||||
buttonText="Become a Creator"
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
useGetV2GetSpecificAgent,
|
||||
useGetV2ListStoreAgents,
|
||||
} from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { MarketplaceAgentPageParams } from "../../agent/[creator]/[slug]/page";
|
||||
import { useGetV2GetAgentByStoreId } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { StoreAgentsResponse } from "@/app/api/__generated__/models/storeAgentsResponse";
|
||||
import { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
|
||||
export const useMainAgentPage = ({
|
||||
params,
|
||||
}: {
|
||||
params: MarketplaceAgentPageParams;
|
||||
}) => {
|
||||
const creator_lower = params.creator.toLowerCase();
|
||||
const { user } = useSupabase();
|
||||
const {
|
||||
data: agent,
|
||||
isLoading: isAgentLoading,
|
||||
isError: isAgentError,
|
||||
} = useGetV2GetSpecificAgent(creator_lower, params.slug, {
|
||||
query: {
|
||||
select: (x) => {
|
||||
return x.data as StoreAgentDetails;
|
||||
},
|
||||
},
|
||||
});
|
||||
const {
|
||||
data: otherAgents,
|
||||
isLoading: isOtherAgentsLoading,
|
||||
isError: isOtherAgentsError,
|
||||
} = useGetV2ListStoreAgents(
|
||||
{ creator: creator_lower },
|
||||
{
|
||||
query: {
|
||||
select: (x) => {
|
||||
return x.data as StoreAgentsResponse;
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const {
|
||||
data: similarAgents,
|
||||
isLoading: isSimilarAgentsLoading,
|
||||
isError: isSimilarAgentsError,
|
||||
} = useGetV2ListStoreAgents(
|
||||
{ search_query: params.slug.replace(/-/g, " ") },
|
||||
{
|
||||
query: {
|
||||
select: (x) => {
|
||||
return x.data as StoreAgentsResponse;
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const {
|
||||
data: libraryAgent,
|
||||
isLoading: isLibraryAgentLoading,
|
||||
isError: isLibraryAgentError,
|
||||
} = useGetV2GetAgentByStoreId(agent?.active_version_id ?? "", {
|
||||
query: {
|
||||
select: (x) => {
|
||||
return x.data as LibraryAgent;
|
||||
},
|
||||
enabled: !!user && !!agent?.active_version_id,
|
||||
},
|
||||
});
|
||||
|
||||
const isLoading =
|
||||
isAgentLoading ||
|
||||
isOtherAgentsLoading ||
|
||||
isSimilarAgentsLoading ||
|
||||
isLibraryAgentLoading;
|
||||
|
||||
const hasError =
|
||||
isAgentError ||
|
||||
isOtherAgentsError ||
|
||||
isSimilarAgentsError ||
|
||||
isLibraryAgentError;
|
||||
|
||||
return {
|
||||
agent,
|
||||
otherAgents,
|
||||
similarAgents,
|
||||
libraryAgent,
|
||||
isLoading,
|
||||
hasError,
|
||||
user,
|
||||
};
|
||||
};
|
||||
@@ -43,7 +43,7 @@ export const MainCreatorPage = ({ params }: MainCreatorPageProps) => {
|
||||
<main className="mt-5 px-4">
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ name: "Store", link: "/marketplace" },
|
||||
{ name: "Marketplace", link: "/marketplace" },
|
||||
{ name: creator.name, link: "#" },
|
||||
]}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user