Reorganize marketplace components and update imports

This commit is contained in:
abhi1992002
2025-06-29 11:20:10 +05:30
parent 067ee41c58
commit 6bbcd44a92
45 changed files with 1190 additions and 888 deletions

View File

@@ -1,12 +1,12 @@
import BackendAPI from "@/lib/autogpt-server-api";
import { BreadCrumbs } from "@/components/agptui/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 { BreadCrumbs } from "@/app/(platform)/marketplace/components/BreadCrumbs/BreadCrumbs";
import { AgentInfo } from "@/app/(platform)/marketplace/components/AgentInfo/AgentInfo";
import { AgentImages } from "@/app/(platform)/marketplace/components/AgentImages/AgentImages";
import { BecomeACreator } from "@/app/(platform)/marketplace/components/BecomeACreator/BecomeACreator";
import { Separator } from "@/components/ui/separator";
import { Metadata } from "next";
import { getServerUser } from "@/lib/supabase/server/getServerUser";
import { AgentsSection } from "../../../components/AgentsSection/AgentsSection";
// Force dynamic rendering to avoid static generation issues with cookies
export const dynamic = "force-dynamic";

View File

@@ -1,25 +1,10 @@
import * as React from "react";
"use client";
import Image from "next/image";
import { PlayIcon } from "@radix-ui/react-icons";
import { Button } from "./Button";
const isValidVideoFile = (url: string): boolean => {
const videoExtensions = /\.(mp4|webm|ogg)$/i;
return videoExtensions.test(url);
};
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);
};
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;
};
import { Button } from "../../../../../components/agptui/Button";
import { getYouTubeVideoId, isValidVideoFile, isValidVideoUrl } from "./helper";
import { memo } from "react";
import { useAgentImageItem } from "./useAgentImageItem";
interface AgentImageItemProps {
image: string;
@@ -29,20 +14,15 @@ interface AgentImageItemProps {
handlePause: (index: number) => void;
}
export const AgentImageItem: React.FC<AgentImageItemProps> = React.memo(
({ image, index, playingVideoIndex, handlePlay, handlePause }) => {
const videoRef = React.useRef<HTMLVideoElement>(null);
React.useEffect(() => {
if (
playingVideoIndex !== index &&
videoRef.current &&
!videoRef.current.paused
) {
videoRef.current.pause();
}
}, [playingVideoIndex, index]);
export const AgentImageItem = memo(
({
image,
index,
playingVideoIndex,
handlePlay,
handlePause,
}: AgentImageItemProps) => {
const { videoRef } = useAgentImageItem({ playingVideoIndex, index });
const isVideoFile = isValidVideoFile(image);
return (

View File

@@ -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;
};

View File

@@ -0,0 +1,27 @@
import { useEffect, useRef } from "react";
interface useAgentImageItem {
playingVideoIndex: number | null;
index: number;
}
export const useAgentImageItem = ({
playingVideoIndex,
index,
}: useAgentImageItem) => {
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
if (
playingVideoIndex !== index &&
videoRef.current &&
!videoRef.current.paused
) {
videoRef.current.pause();
}
}, [playingVideoIndex, index]);
return {
videoRef,
};
};

View File

@@ -1,29 +1,14 @@
"use client";
import * as React from "react";
import { AgentImageItem } from "./AgentImageItem";
import { AgentImageItem } from "../AgentImageItem/AgentImageItem";
import { useAgentImages } from "./useAgentImages";
interface AgentImagesProps {
images: string[];
}
export const AgentImages: React.FC<AgentImagesProps> = ({ images }) => {
const [playingVideoIndex, setPlayingVideoIndex] = React.useState<
number | null
>(null);
const handlePlay = React.useCallback((index: number) => {
setPlayingVideoIndex(index);
}, []);
const handlePause = React.useCallback(
(index: number) => {
if (playingVideoIndex === index) {
setPlayingVideoIndex(null);
}
},
[playingVideoIndex],
);
const { handlePause, handlePlay, playingVideoIndex } = useAgentImages();
return (
<div className="w-full overflow-y-auto bg-white px-2 dark:bg-transparent lg:w-[56.25rem]">

View File

@@ -0,0 +1,21 @@
import { useCallback, useState } from "react";
export const useAgentImages = () => {
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 { handlePlay, handlePause, playingVideoIndex };
};

View File

@@ -2,15 +2,12 @@
import { StarRatingIcons } from "@/components/ui/icons";
import { Separator } from "@/components/ui/separator";
import BackendAPI, { LibraryAgent } from "@/lib/autogpt-server-api";
import { useRouter } from "next/navigation";
import { LibraryAgent } from "@/lib/autogpt-server-api";
import Link from "next/link";
import { useToast } from "@/components/ui/use-toast";
import { useOnboarding } from "../onboarding/onboarding-provider";
import { User } from "@supabase/supabase-js";
import { cn } from "@/lib/utils";
import { FC, useCallback, useMemo, useState } from "react";
import { useAgentInfo } from "./useAgentInfo";
interface AgentInfoProps {
user: User | null;
@@ -27,7 +24,7 @@ interface AgentInfoProps {
libraryAgent: LibraryAgent | null;
}
export const AgentInfo: FC<AgentInfoProps> = ({
export const AgentInfo = ({
user,
name,
creator,
@@ -40,89 +37,11 @@ export const AgentInfo: FC<AgentInfoProps> = ({
version,
storeListingVersionId,
libraryAgent,
}) => {
const router = useRouter();
const api = useMemo(() => new BackendAPI(), []);
const { toast } = useToast();
const { completeStep } = useOnboarding();
const [adding, setAdding] = useState(false);
const [downloading, setDownloading] = useState(false);
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 handleDownload = useCallback(async () => {
const downloadAgent = async (): Promise<void> => {
setDownloading(true);
try {
const file = await api.downloadStoreAgent(storeListingVersionId);
// Similar to Marketplace v1
const jsonData = JSON.stringify(file, null, 2);
// Create a Blob from the file content
const blob = new Blob([jsonData], { type: "application/json" });
// Create a temporary URL for the Blob
const url = window.URL.createObjectURL(blob);
// Create a temporary anchor element
const a = document.createElement("a");
a.href = url;
a.download = `agent_${storeListingVersionId}.json`; // Set the filename
// Append the anchor to the body, click it, and remove it
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// Revoke the temporary URL
window.URL.revokeObjectURL(url);
toast({
title: "Download Complete",
description: "Your agent has been successfully downloaded.",
});
} catch (error) {
console.error(`Error downloading agent:`, error);
toast({
title: "Error",
description: "Failed to download agent. Please try again.",
variant: "destructive",
});
}
};
await downloadAgent();
setDownloading(false);
}, [setDownloading, api, storeListingVersionId, toast]);
}: AgentInfoProps) => {
const { adding, downloading, libraryAction, handleDownload } = useAgentInfo({
storeListingVersionId,
libraryAgent,
});
return (
<div className="w-full max-w-[396px] px-4 sm:px-6 lg:w-[396px] lg:px-0">

View File

@@ -0,0 +1,107 @@
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
import { useToast } from "@/components/ui/use-toast";
import BackendAPI, { LibraryAgent } from "@/lib/autogpt-server-api";
import { useRouter } from "next/navigation";
import { useState } from "react";
interface useAgentInfoProps {
storeListingVersionId: string;
libraryAgent: LibraryAgent | null;
}
export const useAgentInfo = ({
storeListingVersionId,
libraryAgent,
}: useAgentInfoProps) => {
const router = useRouter();
const api = new BackendAPI();
const { toast } = useToast();
const { completeStep } = useOnboarding();
const [adding, setAdding] = useState(false);
const [downloading, setDownloading] = useState(false);
const libraryAction = 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",
});
}
};
const handleDownload = async () => {
const downloadAgent = async (): Promise<void> => {
setDownloading(true);
try {
const file = await api.downloadStoreAgent(storeListingVersionId);
// Similar to Marketplace v1
const jsonData = JSON.stringify(file, null, 2);
// Create a Blob from the file content
const blob = new Blob([jsonData], { type: "application/json" });
// Create a temporary URL for the Blob
const url = window.URL.createObjectURL(blob);
// Create a temporary anchor element
const a = document.createElement("a");
a.href = url;
a.download = `agent_${storeListingVersionId}.json`; // Set the filename
// Append the anchor to the body, click it, and remove it
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// Revoke the temporary URL
window.URL.revokeObjectURL(url);
toast({
title: "Download Complete",
description: "Your agent has been successfully downloaded.",
});
} catch (error) {
console.error(`Error downloading agent:`, error);
toast({
title: "Error",
description: "Failed to download agent. Please try again.",
variant: "destructive",
});
}
};
await downloadAgent();
setDownloading(false);
};
return {
adding,
downloading,
libraryAction,
handleDownload,
};
};

View File

@@ -1,13 +1,12 @@
"use client";
import * as React from "react";
import { StoreCard } from "@/components/agptui/StoreCard";
import { StoreCard } from "@/app/(platform)/marketplace/components/StoreCard/StoreCard";
import {
Carousel,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
import { useRouter } from "next/navigation";
import { useAgentsSection } from "./useAgentsSection";
export interface Agent {
slug: string;
@@ -28,22 +27,15 @@ interface AgentsSectionProps {
margin?: string;
}
export const AgentsSection: React.FC<AgentsSectionProps> = ({
export const AgentsSection = ({
sectionTitle,
agents: allAgents,
hideAvatars = false,
margin = "24px",
}) => {
const router = useRouter();
// TODO: Update this when we have pagination
}: AgentsSectionProps) => {
// TODO: Update this when we have pagination and shifts to useAgentsSection
const displayedAgents = allAgents;
const handleCardClick = (creator: string, slug: string) => {
router.push(
`/marketplace/agent/${encodeURIComponent(creator)}/${encodeURIComponent(slug)}`,
);
};
const { handleCardClick } = useAgentsSection();
return (
<div className="flex flex-col items-center justify-center">

View File

@@ -0,0 +1,14 @@
import { useRouter } from "next/navigation";
// TODO : Need to add more logic for pagination in future
export const useAgentsSection = () => {
const router = useRouter();
const handleCardClick = (creator: string, slug: string) => {
router.push(
`/marketplace/agent/${encodeURIComponent(creator)}/${encodeURIComponent(slug)}`,
);
};
return { handleCardClick };
};

View File

@@ -1,7 +1,6 @@
"use client";
import * as React from "react";
import { PublishAgentPopout } from "./composite/PublishAgentPopout";
import { PublishAgentPopout } from "../PublishAgentPopout/PublishAgentPopout";
interface BecomeACreatorProps {
title?: string;
description?: string;
@@ -9,12 +8,12 @@ interface BecomeACreatorProps {
onButtonClick?: () => void;
}
export const BecomeACreator: React.FC<BecomeACreatorProps> = ({
export const BecomeACreator = ({
title = "Become a creator",
description = "Join a community where your AI creations can inspire, engage, and be downloaded by users around the world.",
buttonText = "Upload your agent",
onButtonClick,
}) => {
}: BecomeACreatorProps) => {
const handleButtonClick = () => {
onButtonClick?.();
};

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import Link from "next/link";
import { Fragment } from "react";
interface BreadcrumbItem {
name: string;
@@ -10,20 +10,12 @@ interface BreadCrumbsProps {
items: BreadcrumbItem[];
}
export const BreadCrumbs: React.FC<BreadCrumbsProps> = ({ items }) => {
export const BreadCrumbs = ({ items }: BreadCrumbsProps) => {
return (
<div className="flex items-center gap-4">
{/*
Commented out for now, but keeping until we have approval to remove
<button className="flex h-12 w-12 items-center justify-center rounded-full border border-neutral-200 transition-colors hover:bg-neutral-50 dark:border-neutral-700 dark:hover:bg-neutral-800">
<IconLeftArrow className="h-5 w-5 text-neutral-900 dark:text-neutral-100" />
</button>
<button className="flex h-12 w-12 items-center justify-center rounded-full border border-neutral-200 transition-colors hover:bg-neutral-50 dark:border-neutral-700 dark:hover:bg-neutral-800">
<IconRightArrow className="h-5 w-5 text-neutral-900 dark:text-neutral-100" />
</button> */}
<div className="flex h-auto flex-wrap items-center justify-start gap-4 rounded-[5rem] dark:bg-transparent">
{items.map((item, index) => (
<React.Fragment key={index}>
<Fragment key={index}>
<Link href={item.link}>
<span className="rounded py-1 pr-2 text-xl font-medium leading-9 tracking-tight text-[#272727] transition-colors duration-200 hover:text-gray-400 dark:text-neutral-100 dark:hover:text-gray-500">
{item.name}
@@ -34,7 +26,7 @@ export const BreadCrumbs: React.FC<BreadCrumbsProps> = ({ items }) => {
/
</span>
)}
</React.Fragment>
</Fragment>
))}
</div>
</div>

View File

@@ -1,12 +1,5 @@
import * as React from "react";
import Image from "next/image";
const BACKGROUND_COLORS = [
"bg-amber-100 dark:bg-amber-800", // #fef3c7 / #92400e
"bg-violet-100 dark:bg-violet-800", // #ede9fe / #5b21b6
"bg-green-100 dark:bg-green-800", // #dcfce7 / #065f46
"bg-blue-100 dark:bg-blue-800", // #dbeafe / #1e3a8a
];
import { backgroundColor } from "./helper";
interface CreatorCardProps {
creatorName: string;
@@ -17,19 +10,17 @@ interface CreatorCardProps {
index: number;
}
export const CreatorCard: React.FC<CreatorCardProps> = ({
export const CreatorCard = ({
creatorName,
creatorImage,
bio,
agentsUploaded,
onClick,
index,
}) => {
const backgroundColor = BACKGROUND_COLORS[index % BACKGROUND_COLORS.length];
}: CreatorCardProps) => {
return (
<div
className={`h-[264px] w-full px-[18px] pb-5 pt-6 ${backgroundColor} inline-flex cursor-pointer flex-col items-start justify-start gap-3.5 rounded-[26px] transition-all duration-200 hover:brightness-95`}
className={`h-[264px] w-full px-[18px] pb-5 pt-6 ${backgroundColor(index)} inline-flex cursor-pointer flex-col items-start justify-start gap-3.5 rounded-[26px] transition-all duration-200 hover:brightness-95`}
onClick={onClick}
data-testid="creator-card"
>

View File

@@ -0,0 +1,8 @@
const BACKGROUND_COLORS = [
"bg-amber-100 dark:bg-amber-800", // #fef3c7 / #92400e
"bg-violet-100 dark:bg-violet-800", // #ede9fe / #5b21b6
"bg-green-100 dark:bg-green-800", // #dcfce7 / #065f46
"bg-blue-100 dark:bg-blue-800", // #dbeafe / #1e3a8a
];
export const backgroundColor = (index: number) =>
BACKGROUND_COLORS[index % BACKGROUND_COLORS.length];

View File

@@ -1,4 +1,3 @@
import * as React from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { StarRatingIcons } from "@/components/ui/icons";
@@ -11,14 +10,14 @@ interface CreatorInfoCardProps {
totalRuns: number;
}
export const CreatorInfoCard: React.FC<CreatorInfoCardProps> = ({
export const CreatorInfoCard = ({
username,
handle,
avatarSrc,
categories,
averageRating,
totalRuns,
}) => {
}: CreatorInfoCardProps) => {
return (
<div
className="inline-flex h-auto min-h-[500px] w-full max-w-[440px] flex-col items-start justify-between rounded-[26px] bg-violet-100 p-4 dark:bg-violet-900 sm:h-[632px] sm:w-[440px] sm:p-6"

View File

@@ -1,11 +1,11 @@
import * as React from "react";
import { getIconForSocial } from "@/components/ui/icons";
import { Fragment } from "react";
interface CreatorLinksProps {
links: string[];
}
export const CreatorLinks: React.FC<CreatorLinksProps> = ({ links }) => {
export const CreatorLinks = ({ links }: CreatorLinksProps) => {
if (!links || links.length === 0) {
return null;
}
@@ -35,7 +35,7 @@ export const CreatorLinks: React.FC<CreatorLinksProps> = ({ links }) => {
</div>
<div className="flex w-full flex-wrap gap-3">
{links.map((link, index) => (
<React.Fragment key={index}>{renderLinkButton(link)}</React.Fragment>
<Fragment key={index}>{renderLinkButton(link)}</Fragment>
))}
</div>
</div>

View File

@@ -16,10 +16,11 @@ interface FeaturedStoreCardProps {
backgroundColor: string;
}
export const FeaturedAgentCard: React.FC<FeaturedStoreCardProps> = ({
export const FeaturedAgentCard = ({
agent,
backgroundColor,
}) => {
}: FeaturedStoreCardProps) => {
// TODO: Need to use group for hover
const [isHovered, setIsHovered] = useState(false);
return (

View File

@@ -1,8 +1,7 @@
"use client";
import * as React from "react";
import { CreatorCard } from "@/components/agptui/CreatorCard";
import { useRouter } from "next/navigation";
import { CreatorCard } from "@/app/(platform)/marketplace/components/CreatorCard/CreatorCard";
import { useFeaturedCreators } from "./useFeaturedCreators";
export interface FeaturedCreator {
name: string;
@@ -17,19 +16,13 @@ interface FeaturedCreatorsProps {
featuredCreators: FeaturedCreator[];
}
export const FeaturedCreators: React.FC<FeaturedCreatorsProps> = ({
export const FeaturedCreators = ({
featuredCreators,
title = "Featured Creators",
}) => {
const router = useRouter();
const handleCardClick = (creator: string) => {
router.push(`/marketplace/creator/${encodeURIComponent(creator)}`);
};
// Only show first 4 creators
const displayedCreators = featuredCreators.slice(0, 4);
}: FeaturedCreatorsProps) => {
const { handleCardClick, displayedCreators } = useFeaturedCreators({
featuredCreators,
});
return (
<div className="flex w-full flex-col items-center justify-center">
<div className="w-full max-w-[1360px]">

View File

@@ -0,0 +1,23 @@
import { useRouter } from "next/navigation";
import { FeaturedCreator } from "./FeaturedCreators";
interface useFeaturedCreatorsProps {
featuredCreators: FeaturedCreator[];
}
export const useFeaturedCreators = ({
featuredCreators,
}: useFeaturedCreatorsProps) => {
const router = useRouter();
const handleCardClick = (creator: string) => {
router.push(`/marketplace/creator/${encodeURIComponent(creator)}`);
};
// Only show first 4 creators
const displayedCreators = featuredCreators.slice(0, 4);
return {
handleCardClick,
displayedCreators,
};
};

View File

@@ -1,7 +1,6 @@
"use client";
import * as React from "react";
import { FeaturedAgentCard } from "@/components/agptui/FeaturedAgentCard";
import { FeaturedAgentCard } from "@/app/(platform)/marketplace/components/FeaturedAgentCard/FeaturedAgentCard";
import {
Carousel,
CarouselContent,
@@ -10,41 +9,19 @@ import {
CarouselNext,
CarouselIndicator,
} from "@/components/ui/carousel";
import { useCallback, useState } from "react";
import { StoreAgent } from "@/lib/autogpt-server-api";
import Link from "next/link";
const BACKGROUND_COLORS = [
"bg-violet-200 dark:bg-violet-800", // #ddd6fe / #5b21b6
"bg-blue-200 dark:bg-blue-800", // #bfdbfe / #1e3a8a
"bg-green-200 dark:bg-green-800", // #bbf7d0 / #065f46
];
import { getBackgroundColor } from "./helper";
import { useFeaturedSection } from "./useFeaturedSection";
interface FeaturedSectionProps {
featuredAgents: StoreAgent[];
}
export const FeaturedSection: React.FC<FeaturedSectionProps> = ({
featuredAgents,
}) => {
const [_, setCurrentSlide] = useState(0);
const handlePrevSlide = useCallback(() => {
setCurrentSlide((prev) =>
prev === 0 ? featuredAgents.length - 1 : prev - 1,
);
}, [featuredAgents.length]);
const handleNextSlide = useCallback(() => {
setCurrentSlide((prev) =>
prev === featuredAgents.length - 1 ? 0 : prev + 1,
);
}, [featuredAgents.length]);
const getBackgroundColor = (index: number) => {
return BACKGROUND_COLORS[index % BACKGROUND_COLORS.length];
};
export const FeaturedSection = ({ featuredAgents }: FeaturedSectionProps) => {
const { handleNextSlide, handlePrevSlide } = useFeaturedSection({
featuredAgents,
});
return (
<section className="w-full">
<h2 className="mb-8 font-poppins text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200">

View File

@@ -0,0 +1,9 @@
const BACKGROUND_COLORS = [
"bg-violet-200 dark:bg-violet-800", // #ddd6fe / #5b21b6
"bg-blue-200 dark:bg-blue-800", // #bfdbfe / #1e3a8a
"bg-green-200 dark:bg-green-800", // #bbf7d0 / #065f46
];
export const getBackgroundColor = (index: number) => {
return BACKGROUND_COLORS[index % BACKGROUND_COLORS.length];
};

View File

@@ -0,0 +1,29 @@
import { useState } from "react";
import { StoreAgent } from "@/lib/autogpt-server-api";
interface useFeaturedSectionProps {
featuredAgents: StoreAgent[];
}
export const useFeaturedSection = ({
featuredAgents,
}: useFeaturedSectionProps) => {
const [_, setCurrentSlide] = useState(0);
const handlePrevSlide = () => {
setCurrentSlide((prev) =>
prev === 0 ? featuredAgents.length - 1 : prev - 1,
);
};
const handleNextSlide = () => {
setCurrentSlide((prev) =>
prev === featuredAgents.length - 1 ? 0 : prev + 1,
);
};
return {
handleNextSlide,
handlePrevSlide,
};
};

View File

@@ -1,7 +1,7 @@
"use client";
import * as React from "react";
import { Badge } from "@/components/ui/badge";
import { useFilterChips } from "./useFilterChips";
interface FilterChipsProps {
badges: string[];
@@ -9,31 +9,18 @@ interface FilterChipsProps {
multiSelect?: boolean;
}
/** FilterChips is a component that allows the user to select filters from a list of badges. It is used on the Marketplace home page */
export const FilterChips: React.FC<FilterChipsProps> = ({
// Some flaws in its logic
// TODO: This needs to be fixed
export const FilterChips = ({
badges,
onFilterChange,
multiSelect = true,
}) => {
const [selectedFilters, setSelectedFilters] = React.useState<string[]>([]);
const handleBadgeClick = (badge: string) => {
setSelectedFilters((prevFilters) => {
let newFilters;
if (multiSelect) {
newFilters = prevFilters.includes(badge)
? prevFilters.filter((filter) => filter !== badge)
: [...prevFilters, badge];
} else {
newFilters = prevFilters.includes(badge) ? [] : [badge];
}
if (onFilterChange) {
onFilterChange(newFilters);
}
return newFilters;
});
};
}: FilterChipsProps) => {
const { selectedFilters, handleBadgeClick } = useFilterChips({
multiSelect,
onFilterChange,
});
return (
<div className="flex h-auto min-h-8 flex-wrap items-center justify-center gap-3 lg:min-h-14 lg:justify-start lg:gap-5">

View File

@@ -0,0 +1,37 @@
import { useState } from "react";
interface useFilterChipsProps {
onFilterChange?: (selectedFilters: string[]) => void;
multiSelect?: boolean;
}
export const useFilterChips = ({
onFilterChange,
multiSelect,
}: useFilterChipsProps) => {
const [selectedFilters, setSelectedFilters] = useState<string[]>([]);
const handleBadgeClick = (badge: string) => {
setSelectedFilters((prevFilters) => {
let newFilters;
if (multiSelect) {
newFilters = prevFilters.includes(badge)
? prevFilters.filter((filter) => filter !== badge)
: [...prevFilters, badge];
} else {
newFilters = prevFilters.includes(badge) ? [] : [badge];
}
if (onFilterChange) {
onFilterChange(newFilters);
}
return newFilters;
});
};
return {
selectedFilters,
handleBadgeClick,
};
};

View File

@@ -1,25 +1,11 @@
"use client";
import * as React from "react";
import { SearchBar } from "@/components/agptui/SearchBar";
import { FilterChips } from "@/components/agptui/FilterChips";
import { useRouter } from "next/navigation";
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
export const HeroSection: React.FC = () => {
const router = useRouter();
const { completeStep } = useOnboarding();
// Mark marketplace visit task as completed
React.useEffect(() => {
completeStep("MARKETPLACE_VISIT");
}, [completeStep]);
function onFilterChange(selectedFilters: string[]) {
const encodedTerm = encodeURIComponent(selectedFilters.join(", "));
router.push(`/marketplace/search?searchTerm=${encodedTerm}`);
}
import { FilterChips } from "@/app/(platform)/marketplace/components/FilterChips/FilterChips";
import { SearchBar } from "../SearchBar/SearchBar";
import { useHeroSection } from "./useHeroSection";
export const HeroSection = () => {
const { onFilterChange } = useHeroSection();
return (
<div className="mb-2 mt-8 flex flex-col items-center justify-center px-4 sm:mb-4 sm:mt-12 sm:px-6 md:mb-6 md:mt-16 lg:my-24 lg:px-8 xl:my-16">
<div className="w-full max-w-3xl lg:max-w-4xl xl:max-w-5xl">

View File

@@ -0,0 +1,22 @@
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export const useHeroSection = () => {
const router = useRouter();
const { completeStep } = useOnboarding();
// Mark marketplace visit task as completed
useEffect(() => {
completeStep("MARKETPLACE_VISIT");
}, [completeStep]);
function onFilterChange(selectedFilters: string[]) {
const encodedTerm = encodeURIComponent(selectedFilters.join(", "));
router.push(`/marketplace/search?searchTerm=${encodedTerm}`);
}
return {
onFilterChange,
};
};

View File

@@ -1,9 +1,8 @@
"use client";
import * as React from "react";
import { IconCross } from "../ui/icons";
import { IconCross } from "../../../../../components/ui/icons";
import Image from "next/image";
import { Button } from "../agptui/Button";
import { Button } from "../../../../../components/agptui/Button";
interface PublishAgentAwaitingReviewProps {
agentName: string;
@@ -15,9 +14,7 @@ interface PublishAgentAwaitingReviewProps {
onViewProgress: () => void;
}
export const PublishAgentAwaitingReview: React.FC<
PublishAgentAwaitingReviewProps
> = ({
export const PublishAgentAwaitingReview = ({
agentName,
subheader,
description,
@@ -25,7 +22,7 @@ export const PublishAgentAwaitingReview: React.FC<
onClose,
onDone,
onViewProgress,
}) => {
}: PublishAgentAwaitingReviewProps) => {
return (
<div
className="inline-flex min-h-screen w-full flex-col items-center justify-center rounded-none bg-white dark:bg-neutral-900 sm:h-auto sm:min-h-[824px] sm:rounded-3xl"

View File

@@ -1,11 +1,9 @@
"use client";
import * as React from "react";
import Image from "next/image";
import { Button } from "../agptui/Button";
import { IconCross, IconPlus } from "../ui/icons";
import BackendAPI from "@/lib/autogpt-server-api";
import { toast } from "../ui/use-toast";
import { Button } from "../../../../../components/agptui/Button";
import { IconCross, IconPlus } from "../../../../../components/ui/icons";
import { usePublishAgentSelectInfo } from "./usePublishAgentSelectInfo";
export interface PublishAgentInfoInitialData {
agent_id: string;
@@ -34,142 +32,35 @@ interface PublishAgentInfoProps {
initialData?: PublishAgentInfoInitialData;
}
export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
export const PublishAgentInfo = ({
onBack,
onSubmit,
onClose,
initialData,
}) => {
const [agentId, setAgentId] = React.useState<string | null>(null);
const [images, setImages] = React.useState<string[]>([]);
const [selectedImage, setSelectedImage] = React.useState<string | null>(
initialData?.thumbnailSrc || null,
);
const [title, setTitle] = React.useState(initialData?.title || "");
const [subheader, setSubheader] = React.useState(
initialData?.subheader || "",
);
const [youtubeLink, setYoutubeLink] = React.useState(
initialData?.youtubeLink || "",
);
const [category, setCategory] = React.useState(initialData?.category || "");
const [description, setDescription] = React.useState(
initialData?.description || "",
);
const [slug, setSlug] = React.useState(initialData?.slug || "");
const thumbnailsContainerRef = React.useRef<HTMLDivElement | null>(null);
React.useEffect(() => {
if (initialData) {
setAgentId(initialData.agent_id);
setImagesWithValidation([
...(initialData?.thumbnailSrc ? [initialData.thumbnailSrc] : []),
...(initialData.additionalImages || []),
]);
setSelectedImage(initialData.thumbnailSrc || null);
setTitle(initialData.title);
setSubheader(initialData.subheader);
setYoutubeLink(initialData.youtubeLink);
setCategory(initialData.category);
setDescription(initialData.description);
setSlug(initialData.slug);
}
}, [initialData]);
const setImagesWithValidation = (newImages: string[]) => {
// Remove duplicates
const uniqueImages = Array.from(new Set(newImages));
// Keep only first 5 images
const limitedImages = uniqueImages.slice(0, 5);
setImages(limitedImages);
};
const handleRemoveImage = (indexToRemove: number) => {
const newImages = [...images];
newImages.splice(indexToRemove, 1);
setImagesWithValidation(newImages);
if (newImages[indexToRemove] === selectedImage) {
setSelectedImage(newImages[0] || null);
}
if (newImages.length === 0) {
setSelectedImage(null);
}
};
const handleAddImage = async () => {
if (images.length >= 5) return;
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
// Create a promise that resolves when file is selected
const fileSelected = new Promise<File | null>((resolve) => {
input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
resolve(file || null);
};
});
// Trigger file selection
input.click();
// Wait for file selection
const file = await fileSelected;
if (!file) return;
try {
const api = new BackendAPI();
const imageUrl = (await api.uploadStoreSubmissionMedia(file)).replace(
/^"(.*)"$/,
"$1",
);
setImagesWithValidation([...images, imageUrl]);
if (!selectedImage) {
setSelectedImage(imageUrl);
}
} catch (error) {
toast({
title: "Failed to upload image",
description: `Error: ${error}`,
});
}
};
const [isGenerating, setIsGenerating] = React.useState(false);
const handleGenerateImage = async () => {
if (isGenerating || images.length >= 5) return;
setIsGenerating(true);
try {
const api = new BackendAPI();
if (!agentId) {
throw new Error("Agent ID is required");
}
const { image_url } = await api.generateStoreSubmissionImage(agentId);
setImagesWithValidation([...images, image_url]);
} catch (error) {
console.error("Failed to generate image:", error);
} finally {
setIsGenerating(false);
}
};
const handleSubmit = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
const categories = category ? [category] : [];
onSubmit(
title,
subheader,
slug,
description,
images,
youtubeLink,
categories,
);
};
}: PublishAgentInfoProps) => {
const {
images,
selectedImage,
setSelectedImage,
title,
setTitle,
subheader,
setSubheader,
youtubeLink,
setYoutubeLink,
category,
setCategory,
description,
setDescription,
slug,
setSlug,
isGenerating,
handleGenerateImage,
handleSubmit,
handleAddImage,
handleRemoveImage,
thumbnailsContainerRef,
} = usePublishAgentSelectInfo({ onSubmit, initialData });
return (
<div className="mx-auto flex w-full flex-col rounded-3xl bg-white dark:bg-gray-800">

View File

@@ -0,0 +1,177 @@
import { toast } from "@/components/ui/use-toast";
import BackendAPI from "@/lib/autogpt-server-api";
import { useEffect, useRef, useState } from "react";
import { PublishAgentInfoInitialData } from "./PublishAgentSelectInfo";
interface usePublishAgentInfoProps {
onSubmit: (
name: string,
subHeading: string,
slug: string,
description: string,
imageUrls: string[],
videoUrl: string,
categories: string[],
) => void;
initialData?: PublishAgentInfoInitialData;
}
// Need to combine some states in one state
export const usePublishAgentSelectInfo = ({
onSubmit,
initialData,
}: usePublishAgentInfoProps) => {
const [agentId, setAgentId] = useState<string | null>(null);
const [images, setImages] = useState<string[]>([]);
const [selectedImage, setSelectedImage] = useState<string | null>(
initialData?.thumbnailSrc || null,
);
const [title, setTitle] = useState(initialData?.title || "");
const [subheader, setSubheader] = useState(initialData?.subheader || "");
const [youtubeLink, setYoutubeLink] = useState(
initialData?.youtubeLink || "",
);
const [category, setCategory] = useState(initialData?.category || "");
const [description, setDescription] = useState(
initialData?.description || "",
);
const [slug, setSlug] = useState(initialData?.slug || "");
const thumbnailsContainerRef = useRef<HTMLDivElement | null>(null);
const [isGenerating, setIsGenerating] = useState(false);
useEffect(() => {
if (initialData) {
setAgentId(initialData.agent_id);
setImagesWithValidation([
...(initialData?.thumbnailSrc ? [initialData.thumbnailSrc] : []),
...(initialData.additionalImages || []),
]);
setSelectedImage(initialData.thumbnailSrc || null);
setTitle(initialData.title);
setSubheader(initialData.subheader);
setYoutubeLink(initialData.youtubeLink);
setCategory(initialData.category);
setDescription(initialData.description);
setSlug(initialData.slug);
}
}, [initialData]);
const setImagesWithValidation = (newImages: string[]) => {
// Remove duplicates
const uniqueImages = Array.from(new Set(newImages));
// Keep only first 5 images
const limitedImages = uniqueImages.slice(0, 5);
setImages(limitedImages);
};
const handleRemoveImage = (indexToRemove: number) => {
const newImages = [...images];
newImages.splice(indexToRemove, 1);
setImagesWithValidation(newImages);
if (newImages[indexToRemove] === selectedImage) {
setSelectedImage(newImages[0] || null);
}
if (newImages.length === 0) {
setSelectedImage(null);
}
};
const handleAddImage = async () => {
if (images.length >= 5) return;
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
// Create a promise that resolves when file is selected
const fileSelected = new Promise<File | null>((resolve) => {
input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
resolve(file || null);
};
});
// Trigger file selection
input.click();
// Wait for file selection
const file = await fileSelected;
if (!file) return;
try {
const api = new BackendAPI();
const imageUrl = (await api.uploadStoreSubmissionMedia(file)).replace(
/^"(.*)"$/,
"$1",
);
setImagesWithValidation([...images, imageUrl]);
if (!selectedImage) {
setSelectedImage(imageUrl);
}
} catch (error) {
toast({
title: "Failed to upload image",
description: `Error: ${error}`,
});
}
};
const handleGenerateImage = async () => {
if (isGenerating || images.length >= 5) return;
setIsGenerating(true);
try {
const api = new BackendAPI();
if (!agentId) {
throw new Error("Agent ID is required");
}
const { image_url } = await api.generateStoreSubmissionImage(agentId);
setImagesWithValidation([...images, image_url]);
} catch (error) {
console.error("Failed to generate image:", error);
} finally {
setIsGenerating(false);
}
};
const handleSubmit = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
const categories = category ? [category] : [];
onSubmit(
title,
subheader,
slug,
description,
images,
youtubeLink,
categories,
);
};
return {
images,
selectedImage,
setSelectedImage,
title,
setTitle,
subheader,
setSubheader,
youtubeLink,
setYoutubeLink,
category,
setCategory,
description,
setDescription,
slug,
setSlug,
isGenerating,
handleGenerateImage,
handleSubmit,
handleAddImage,
handleRemoveImage,
thumbnailsContainerRef,
};
};

View File

@@ -0,0 +1,153 @@
"use client";
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverAnchor,
} from "@/components/ui/popover";
import { PublishAgentSelect } from "../PublishAgentSelect/PublishAgentSelect";
import { PublishAgentInfo } from "../PublishAgentInfo/PublishAgentSelectInfo";
import { PublishAgentAwaitingReview } from "../PublishAgentAwaitingReview/PublishAgentAwaitingReview";
import { Button } from "../../../../../components/agptui/Button";
import { StoreSubmissionRequest } from "@/lib/autogpt-server-api";
import { usePublishAgentPopout } from "./usePublishAgentPopout";
interface PublishAgentPopoutProps {
trigger?: React.ReactNode;
openPopout?: boolean;
inputStep?: "select" | "info" | "review";
submissionData?: StoreSubmissionRequest;
}
export const PublishAgentPopout = ({
trigger,
openPopout = false,
inputStep = "select",
submissionData = {
name: "",
sub_heading: "",
slug: "",
description: "",
image_urls: [],
agent_id: "",
agent_version: 0,
categories: [],
},
}: PublishAgentPopoutProps) => {
const {
handleBack,
handleNextFromInfo,
handleNextFromSelect,
handleAgentSelect,
handleClose,
router,
step,
myAgents,
selectedAgent: _,
initialData,
publishData,
open,
setOpen,
popupId,
} = usePublishAgentPopout({ openPopout, inputStep, submissionData });
const renderContent = () => {
switch (step) {
case "select":
return (
<div className="flex min-h-screen items-center justify-center">
<div className="mx-auto flex w-full max-w-[900px] flex-col rounded-3xl bg-white shadow-lg dark:bg-gray-800">
<div className="h-full overflow-y-auto">
<PublishAgentSelect
agents={
myAgents?.agents
.map((agent) => ({
name: agent.agent_name,
id: agent.agent_id,
version: agent.agent_version,
lastEdited: agent.last_edited,
imageSrc:
agent.agent_image || "https://picsum.photos/300/200",
}))
.sort(
(a, b) =>
new Date(b.lastEdited).getTime() -
new Date(a.lastEdited).getTime(),
) || []
}
onSelect={handleAgentSelect}
onCancel={handleClose}
onNext={handleNextFromSelect}
onClose={handleClose}
onOpenBuilder={() => router.push("/build")}
/>
</div>
</div>
</div>
);
case "info":
return (
<div className="flex min-h-screen items-center justify-center">
<div className="mx-auto flex w-full max-w-[900px] flex-col rounded-3xl bg-white shadow-lg dark:bg-gray-800">
<div className="h-[700px] overflow-y-auto">
<PublishAgentInfo
onBack={handleBack}
onSubmit={handleNextFromInfo}
onClose={handleClose}
initialData={initialData}
/>
</div>
</div>
</div>
);
case "review":
return publishData ? (
<div className="flex justify-center">
<div className="mx-auto flex w-full max-w-[900px] flex-col rounded-3xl bg-white shadow-lg dark:bg-gray-800">
<div className="h-[600px] overflow-y-auto">
<PublishAgentAwaitingReview
agentName={publishData.name}
subheader={publishData.sub_heading}
description={publishData.description}
thumbnailSrc={publishData.image_urls[0]}
onClose={handleClose}
onDone={handleClose}
onViewProgress={() => {
router.push("/profile/dashboard");
handleClose();
}}
/>
</div>
</div>
</div>
) : null;
}
};
return (
<Popover
open={open}
onOpenChange={(isOpen) => {
if (isOpen !== open) {
setOpen(isOpen);
}
}}
>
<PopoverTrigger asChild>
{trigger || <Button>Publish Agent</Button>}
</PopoverTrigger>
<PopoverAnchor asChild>
<div className="fixed left-0 top-0 hidden h-screen w-screen items-center justify-center"></div>
</PopoverAnchor>
<PopoverContent
id={popupId}
align="center"
className="z-50 h-screen w-screen bg-transparent"
>
{renderContent()}
</PopoverContent>
</Popover>
);
};

View File

@@ -0,0 +1,196 @@
import { useEffect, useId, useState } from "react";
import {
MyAgentsResponse,
StoreSubmissionRequest,
} from "@/lib/autogpt-server-api";
import { PublishAgentInfoInitialData } from "../PublishAgentInfo/PublishAgentSelectInfo";
import { useRouter } from "next/navigation";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { useToast } from "@/components/ui/use-toast";
interface usePublishAgentPopout {
openPopout: boolean;
inputStep: "select" | "info" | "review";
submissionData: StoreSubmissionRequest;
}
export const usePublishAgentPopout = ({
openPopout,
inputStep,
submissionData,
}: usePublishAgentPopout) => {
const { toast } = useToast();
const [step, setStep] = useState<"select" | "info" | "review">(inputStep);
const [myAgents, setMyAgents] = useState<MyAgentsResponse | null>(null);
const [_, setSelectedAgent] = useState<string | null>(null);
const [initialData, setInitialData] = useState<PublishAgentInfoInitialData>({
agent_id: "",
title: "",
subheader: "",
slug: "",
thumbnailSrc: "",
youtubeLink: "",
category: "",
description: "",
});
const [publishData, setPublishData] =
useState<StoreSubmissionRequest>(submissionData);
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
const [selectedAgentVersion, setSelectedAgentVersion] = useState<
number | null
>(null);
const [open, setOpen] = useState(false);
const popupId = useId();
const router = useRouter();
const api = useBackendAPI();
useEffect(() => {
setOpen(openPopout);
setStep(inputStep);
setPublishData(submissionData);
}, [openPopout]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (open) {
const loadMyAgents = async () => {
try {
const response = await api.getMyAgents();
setMyAgents(response);
} catch (error) {
console.error("Failed to load my agents:", error);
}
};
loadMyAgents();
}
}, [open, api]);
const handleClose = () => {
setStep("select");
setSelectedAgent(null);
setPublishData({
name: "",
sub_heading: "",
description: "",
image_urls: [],
agent_id: "",
agent_version: 0,
slug: "",
categories: [],
});
setOpen(false);
};
const handleAgentSelect = (agentName: string) => {
setSelectedAgent(agentName);
};
const handleNextFromSelect = (agentId: string, agentVersion: number) => {
const selectedAgentData = myAgents?.agents.find(
(agent) => agent.agent_id === agentId,
);
const name = selectedAgentData?.agent_name || "";
const description = selectedAgentData?.description || "";
setInitialData({
agent_id: agentId,
title: name,
subheader: "",
description: description,
thumbnailSrc: selectedAgentData?.agent_image || "",
youtubeLink: "",
category: "",
slug: name.replace(/ /g, "-"),
additionalImages: [],
});
setStep("info");
setSelectedAgentId(agentId);
setSelectedAgentVersion(agentVersion);
};
const handleNextFromInfo = async (
name: string,
subHeading: string,
slug: string,
description: string,
imageUrls: string[],
videoUrl: string,
categories: string[],
) => {
const missingFields: string[] = [];
if (!name) missingFields.push("Name");
if (!subHeading) missingFields.push("Sub-heading");
if (!description) missingFields.push("Description");
if (!imageUrls.length) missingFields.push("Image");
if (!categories.filter(Boolean).length) missingFields.push("Categories");
if (missingFields.length > 0) {
toast({
title: "Missing Required Fields",
description: `Please fill in: ${missingFields.join(", ")}`,
duration: 3000,
});
return;
}
const filteredCategories = categories.filter(Boolean);
setPublishData({
name,
sub_heading: subHeading,
description,
image_urls: imageUrls,
video_url: videoUrl,
agent_id: selectedAgentId || "",
agent_version: selectedAgentVersion || 0,
slug,
categories: filteredCategories,
});
// Create store submission
try {
await api.createStoreSubmission({
name: name,
sub_heading: subHeading,
description: description,
image_urls: imageUrls,
video_url: videoUrl,
agent_id: selectedAgentId || "",
agent_version: selectedAgentVersion || 0,
slug: slug.replace(/\s+/g, "-"),
categories: filteredCategories,
});
} catch (error) {
console.error("Error creating store submission:", error);
}
setStep("review");
};
const handleBack = () => {
if (step === "info") {
setStep("select");
} else if (step === "review") {
setStep("info");
}
};
return {
handleBack,
handleNextFromInfo,
handleNextFromSelect,
handleAgentSelect,
handleClose,
router,
step,
myAgents,
selectedAgent: _,
initialData,
publishData,
open,
setOpen,
popupId,
};
};

View File

@@ -1,9 +1,9 @@
"use client";
import * as React from "react";
import Image from "next/image";
import { Button } from "../agptui/Button";
import { IconCross } from "../ui/icons";
import { Button } from "../../../../../components/agptui/Button";
import { IconCross } from "../../../../../components/ui/icons";
import { usePublishAgentSelect } from "./usePublishAgentSelect";
export interface Agent {
name: string;
@@ -22,30 +22,16 @@ interface PublishAgentSelectProps {
onOpenBuilder: () => void;
}
export const PublishAgentSelect: React.FC<PublishAgentSelectProps> = ({
export const PublishAgentSelect = ({
agents,
onSelect,
onCancel,
onNext,
onClose,
onOpenBuilder,
}) => {
const [selectedAgentId, setSelectedAgentId] = React.useState<string | null>(
null,
);
const [selectedAgentVersion, setSelectedAgentVersion] = React.useState<
number | null
>(null);
const handleAgentClick = (
agentName: string,
agentId: string,
agentVersion: number,
) => {
setSelectedAgentId(agentId);
setSelectedAgentVersion(agentVersion);
onSelect(agentId, agentVersion);
};
}: PublishAgentSelectProps) => {
const { selectedAgentId, selectedAgentVersion, handleAgentClick } =
usePublishAgentSelect({ onSelect });
return (
<div className="mx-auto flex w-full max-w-[900px] flex-col rounded-3xl bg-white shadow-lg dark:bg-gray-800">

View File

@@ -0,0 +1,30 @@
import { useState } from "react";
interface usePublishAgentSelectProps {
onSelect: (agentId: string, agentVersion: number) => void;
}
export const usePublishAgentSelect = ({
onSelect,
}: usePublishAgentSelectProps) => {
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
const [selectedAgentVersion, setSelectedAgentVersion] = useState<
number | null
>(null);
const handleAgentClick = (
agentName: string,
agentId: string,
agentVersion: number,
) => {
setSelectedAgentId(agentId);
setSelectedAgentVersion(agentVersion);
onSelect(agentId, agentVersion);
};
return {
selectedAgentId,
selectedAgentVersion,
handleAgentClick,
};
};

View File

@@ -1,9 +1,7 @@
"use client";
import * as React from "react";
import { useRouter } from "next/navigation";
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
import { useSearchbar } from "./useSearchBar";
interface SearchBarProps {
placeholder?: string;
@@ -15,8 +13,7 @@ interface SearchBarProps {
height?: string;
}
/** SearchBar component for user input and search functionality. */
export const SearchBar: React.FC<SearchBarProps> = ({
export const SearchBar = ({
placeholder = 'Search for tasks like "optimise SEO"',
backgroundColor = "bg-neutral-100 dark:bg-neutral-800",
iconColor = "text-[#646464] dark:text-neutral-400",
@@ -24,21 +21,8 @@ export const SearchBar: React.FC<SearchBarProps> = ({
placeholderColor = "text-[#707070] dark:text-neutral-400",
width = "w-9/10 lg:w-[56.25rem]",
height = "h-[60px]",
}) => {
const router = useRouter();
const [searchQuery, setSearchQuery] = React.useState("");
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log(searchQuery);
if (searchQuery.trim()) {
// Encode the search term and navigate to the desired path
const encodedTerm = encodeURIComponent(searchQuery);
router.push(`/marketplace/search?searchTerm=${encodedTerm}`);
}
};
}: SearchBarProps) => {
const { handleSubmit, setSearchQuery, searchQuery } = useSearchbar();
return (
<form

View File

@@ -0,0 +1,25 @@
import { useRouter } from "next/navigation";
import { useState } from "react";
export const useSearchbar = () => {
const router = useRouter();
const [searchQuery, setSearchQuery] = useState("");
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log(searchQuery);
if (searchQuery.trim()) {
// Encode the search term and navigate to the desired path
const encodedTerm = encodeURIComponent(searchQuery);
router.push(`/marketplace/search?searchTerm=${encodedTerm}`);
}
};
return {
handleSubmit,
setSearchQuery,
searchQuery,
};
};

View File

@@ -1,8 +1,8 @@
"use client";
import * as React from "react";
import { useSearchFilterChips } from "./useSearchFilterChips";
interface FilterOption {
export interface FilterOption {
label: string;
count: number;
value: string;
@@ -15,25 +15,18 @@ interface SearchFilterChipsProps {
onFilterChange?: (value: string) => void;
}
export const SearchFilterChips: React.FC<SearchFilterChipsProps> = ({
export const SearchFilterChips = ({
totalCount = 10,
agentsCount = 8,
creatorsCount = 2,
onFilterChange,
}) => {
const [selected, setSelected] = React.useState("all");
const filters: FilterOption[] = [
{ label: "All", count: totalCount, value: "all" },
{ label: "Agents", count: agentsCount, value: "agents" },
{ label: "Creators", count: creatorsCount, value: "creators" },
];
const handleFilterClick = (value: string) => {
setSelected(value);
onFilterChange?.(value);
console.log(`Filter selected: ${value}`);
};
}: SearchFilterChipsProps) => {
const { handleFilterClick, selected, filters } = useSearchFilterChips({
totalCount,
agentsCount,
creatorsCount,
onFilterChange,
});
return (
<div className="flex gap-2.5">

View File

@@ -0,0 +1,34 @@
import { useState } from "react";
import { FilterOption } from "./SearchFilterChips";
interface useSearchFilterChipsProps {
totalCount: number;
agentsCount: number;
creatorsCount: number;
onFilterChange?: (value: string) => void;
}
export const useSearchFilterChips = ({
totalCount,
agentsCount,
creatorsCount,
onFilterChange,
}: useSearchFilterChipsProps) => {
const [selected, setSelected] = useState("all");
const filters: FilterOption[] = [
{ label: "All", count: totalCount, value: "all" },
{ label: "Agents", count: agentsCount, value: "agents" },
{ label: "Creators", count: creatorsCount, value: "creators" },
];
const handleFilterClick = (value: string) => {
setSelected(value);
onFilterChange?.(value);
};
return {
handleFilterClick,
selected,
filters,
};
};

View File

@@ -1,6 +1,5 @@
"use client";
import * as React from "react";
import {
DropdownMenu,
DropdownMenuContent,
@@ -8,6 +7,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ChevronDownIcon } from "@radix-ui/react-icons";
import { useSortDropdown } from "./useSortDropdown";
const sortOptions: SortOption[] = [
{ label: "Most Recent", value: "recent" },
@@ -15,22 +15,17 @@ const sortOptions: SortOption[] = [
{ label: "Highest Rated", value: "rating" },
];
interface SortOption {
export interface SortOption {
label: string;
value: string;
}
export const SortDropdown: React.FC<{
interface SortDropdownProps {
onSort: (sortValue: string) => void;
}> = ({ onSort }) => {
const [selected, setSelected] = React.useState(sortOptions[0]);
const handleSelect = (option: SortOption) => {
setSelected(option);
onSort(option.value);
console.log(`Sorting by: ${option.label} (${option.value})`);
};
}
export const SortDropdown = ({ onSort }: SortDropdownProps) => {
const { selected, handleSelect } = useSortDropdown({ onSort, sortOptions });
return (
<DropdownMenu>
<DropdownMenuTrigger className="flex items-center gap-1.5 focus:outline-none">

View File

@@ -0,0 +1,24 @@
import { useState } from "react";
import { SortOption } from "./SortDropdown";
interface useSortDropdownProps {
onSort: (sortValue: string) => void;
sortOptions: SortOption[];
}
export const useSortDropdown = ({
onSort,
sortOptions,
}: useSortDropdownProps) => {
const [selected, setSelected] = useState(sortOptions[0]);
const handleSelect = (option: SortOption) => {
setSelected(option);
onSort(option.value);
};
return {
selected,
handleSelect,
};
};

View File

@@ -1,4 +1,3 @@
import * as React from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import Image from "next/image";
import { StarRatingIcons } from "@/components/ui/icons";

View File

@@ -1,10 +1,10 @@
import BackendAPI from "@/lib/autogpt-server-api";
import { AgentsSection } from "@/components/agptui/composite/AgentsSection";
import { BreadCrumbs } from "@/components/agptui/BreadCrumbs";
import { BreadCrumbs } from "@/app/(platform)/marketplace/components/BreadCrumbs/BreadCrumbs";
import { Metadata } from "next";
import { CreatorInfoCard } from "@/components/agptui/CreatorInfoCard";
import { CreatorLinks } from "@/components/agptui/CreatorLinks";
import { CreatorInfoCard } from "@/app/(platform)/marketplace/components/CreatorInfoCard/CreatorInfoCard";
import { CreatorLinks } from "@/app/(platform)/marketplace/components/CreatorLinks/CreatorLinks";
import { Separator } from "@/components/ui/separator";
import { AgentsSection } from "../../components/AgentsSection/AgentsSection";
// Force dynamic rendering to avoid static generation issues with cookies
export const dynamic = "force-dynamic";

View File

@@ -1,19 +1,17 @@
import React from "react";
import { HeroSection } from "@/components/agptui/composite/HeroSection";
import { FeaturedSection } from "@/components/agptui/composite/FeaturedSection";
import {
AgentsSection,
Agent,
} from "@/components/agptui/composite/AgentsSection";
import { BecomeACreator } from "@/components/agptui/BecomeACreator";
import {
FeaturedCreators,
FeaturedCreator,
} from "@/components/agptui/composite/FeaturedCreators";
import { Separator } from "@/components/ui/separator";
import { Metadata } from "next";
import { getMarketplaceData } from "./actions";
import { HeroSection } from "./components/HeroSection/HeroSection";
import { Agent, AgentsSection } from "./components/AgentsSection/AgentsSection";
import {
FeaturedCreator,
FeaturedCreators,
} from "./components/FeaturedCreators/FeaturedCreators";
import { BecomeACreator } from "./components/BecomeACreator/BecomeACreator";
import { FeaturedSection } from "./components/FeaturedSection/FeaturedSection";
// Force dynamic rendering to avoid static generation issues with cookies
export const dynamic = "force-dynamic";

View File

@@ -1,14 +1,14 @@
"use client";
import { use, useCallback, useEffect, useState } from "react";
import { AgentsSection } from "@/components/agptui/composite/AgentsSection";
import { SearchBar } from "@/components/agptui/SearchBar";
import { FeaturedCreators } from "@/components/agptui/composite/FeaturedCreators";
import { SearchBar } from "@/app/(platform)/marketplace/components/SearchBar/SearchBar";
import { Separator } from "@/components/ui/separator";
import { SearchFilterChips } from "@/components/agptui/SearchFilterChips";
import { SortDropdown } from "@/components/agptui/SortDropdown";
import { SearchFilterChips } from "@/app/(platform)/marketplace/components/SearchFilterChips/SearchFilterChips";
import { SortDropdown } from "@/app/(platform)/marketplace/components/SortDropdown/SortDropdown";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { Creator, StoreAgent } from "@/lib/autogpt-server-api";
import { FeaturedCreators } from "../components/FeaturedCreators/FeaturedCreators";
import { AgentsSection } from "../components/AgentsSection/AgentsSection";
type MarketplaceSearchPageSearchParams = { searchTerm?: string; sort?: string };

View File

@@ -1541,11 +1541,24 @@
}
}
},
"/api/schedules": {
"/api/graphs/{graph_id}/schedules": {
"post": {
"tags": ["v1", "schedules"],
"summary": "Create execution schedule",
"operationId": "postV1Create execution schedule",
"parameters": [
{
"name": "graph_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"description": "ID of the graph to schedule",
"title": "Graph Id"
},
"description": "ID of the graph to schedule"
}
],
"requestBody": {
"required": true,
"content": {
@@ -1579,17 +1592,14 @@
},
"get": {
"tags": ["v1", "schedules"],
"summary": "List execution schedules",
"operationId": "getV1List execution schedules",
"summary": "List execution schedules for a graph",
"operationId": "getV1List execution schedules for a graph",
"parameters": [
{
"name": "graph_id",
"in": "query",
"required": false,
"schema": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Graph Id"
}
"in": "path",
"required": true,
"schema": { "type": "string", "title": "Graph Id" }
}
],
"responses": {
@@ -1602,7 +1612,7 @@
"items": {
"$ref": "#/components/schemas/GraphExecutionJobInfo"
},
"title": "Response Getv1List Execution Schedules"
"title": "Response Getv1List Execution Schedules For A Graph"
}
}
}
@@ -1618,6 +1628,29 @@
}
}
},
"/api/schedules": {
"get": {
"tags": ["v1", "schedules"],
"summary": "List execution schedules for a user",
"operationId": "getV1List execution schedules for a user",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/GraphExecutionJobInfo"
},
"type": "array",
"title": "Response Getv1List Execution Schedules For A User"
}
}
}
}
}
}
},
"/api/schedules/{schedule_id}": {
"delete": {
"tags": ["v1", "schedules"],
@@ -1628,7 +1661,12 @@
"name": "schedule_id",
"in": "path",
"required": true,
"schema": { "type": "string", "title": "Schedule Id" }
"schema": {
"type": "string",
"description": "ID of the schedule to delete",
"title": "Schedule Id"
},
"description": "ID of the schedule to delete"
}
],
"responses": {
@@ -3199,48 +3237,6 @@
}
}
},
"/api/library/agents/by-graph/{graph_id}": {
"get": {
"tags": ["v2", "library", "private"],
"summary": "Get Library Agent By Graph Id",
"operationId": "getV2GetLibraryAgentByGraphId",
"parameters": [
{
"name": "graph_id",
"in": "path",
"required": true,
"schema": { "type": "string", "title": "Graph Id" }
},
{
"name": "version",
"in": "query",
"required": false,
"schema": {
"anyOf": [{ "type": "integer" }, { "type": "null" }],
"title": "Version"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/LibraryAgent" }
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/api/library/agents/marketplace/{store_listing_version_id}": {
"get": {
"tags": ["v2", "library", "private", "store, library"],
@@ -4187,26 +4183,33 @@
},
"GraphExecutionJobInfo": {
"properties": {
"user_id": { "type": "string", "title": "User Id" },
"graph_id": { "type": "string", "title": "Graph Id" },
"graph_version": { "type": "integer", "title": "Graph Version" },
"cron": { "type": "string", "title": "Cron" },
"input_data": {
"additionalProperties": true,
"type": "object",
"title": "Input Data"
},
"user_id": { "type": "string", "title": "User Id" },
"graph_version": { "type": "integer", "title": "Graph Version" },
"cron": { "type": "string", "title": "Cron" },
"input_credentials": {
"additionalProperties": {
"$ref": "#/components/schemas/CredentialsMetaInput"
},
"type": "object",
"title": "Input Credentials"
},
"id": { "type": "string", "title": "Id" },
"name": { "type": "string", "title": "Name" },
"next_run_time": { "type": "string", "title": "Next Run Time" }
},
"type": "object",
"required": [
"graph_id",
"input_data",
"user_id",
"graph_id",
"graph_version",
"cron",
"input_data",
"id",
"name",
"next_run_time"
@@ -5139,7 +5142,6 @@
"AGENT_INPUT",
"CONGRATS",
"GET_RESULTS",
"RUN_AGENTS",
"MARKETPLACE_VISIT",
"MARKETPLACE_ADD_AGENT",
"MARKETPLACE_RUN_AGENT",
@@ -5636,17 +5638,27 @@
},
"ScheduleCreationRequest": {
"properties": {
"graph_version": {
"anyOf": [{ "type": "integer" }, { "type": "null" }],
"title": "Graph Version"
},
"name": { "type": "string", "title": "Name" },
"cron": { "type": "string", "title": "Cron" },
"input_data": {
"inputs": {
"additionalProperties": true,
"type": "object",
"title": "Input Data"
"title": "Inputs"
},
"graph_id": { "type": "string", "title": "Graph Id" },
"graph_version": { "type": "integer", "title": "Graph Version" }
"credentials": {
"additionalProperties": {
"$ref": "#/components/schemas/CredentialsMetaInput"
},
"type": "object",
"title": "Credentials"
}
},
"type": "object",
"required": ["cron", "input_data", "graph_id", "graph_version"],
"required": ["name", "cron", "inputs"],
"title": "ScheduleCreationRequest"
},
"SetGraphActiveVersion": {

View File

@@ -1,307 +0,0 @@
"use client";
import * as React from "react";
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverAnchor,
} from "@/components/ui/popover";
import { PublishAgentSelect } from "../PublishAgentSelect";
import {
PublishAgentInfo,
PublishAgentInfoInitialData,
} from "../PublishAgentSelectInfo";
import { PublishAgentAwaitingReview } from "../PublishAgentAwaitingReview";
import { Button } from "../Button";
import {
StoreSubmissionRequest,
MyAgentsResponse,
} from "@/lib/autogpt-server-api";
import { useRouter } from "next/navigation";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { useToast } from "@/components/ui/use-toast";
interface PublishAgentPopoutProps {
trigger?: React.ReactNode;
openPopout?: boolean;
inputStep?: "select" | "info" | "review";
submissionData?: StoreSubmissionRequest;
}
export const PublishAgentPopout: React.FC<PublishAgentPopoutProps> = ({
trigger,
openPopout = false,
inputStep = "select",
submissionData = {
name: "",
sub_heading: "",
slug: "",
description: "",
image_urls: [],
agent_id: "",
agent_version: 0,
categories: [],
},
}) => {
const [step, setStep] = React.useState<"select" | "info" | "review">(
inputStep,
);
const [myAgents, setMyAgents] = React.useState<MyAgentsResponse | null>(null);
const [_, setSelectedAgent] = React.useState<string | null>(null);
const [initialData, setInitialData] =
React.useState<PublishAgentInfoInitialData>({
agent_id: "",
title: "",
subheader: "",
slug: "",
thumbnailSrc: "",
youtubeLink: "",
category: "",
description: "",
});
const [publishData, setPublishData] =
React.useState<StoreSubmissionRequest>(submissionData);
const [selectedAgentId, setSelectedAgentId] = React.useState<string | null>(
null,
);
const [selectedAgentVersion, setSelectedAgentVersion] = React.useState<
number | null
>(null);
const [open, setOpen] = React.useState(false);
const popupId = React.useId();
const router = useRouter();
const api = useBackendAPI();
const { toast } = useToast();
React.useEffect(() => {
setOpen(openPopout);
setStep(inputStep);
setPublishData(submissionData);
}, [openPopout]); // eslint-disable-line react-hooks/exhaustive-deps
React.useEffect(() => {
if (open) {
const loadMyAgents = async () => {
try {
const response = await api.getMyAgents();
setMyAgents(response);
} catch (error) {
console.error("Failed to load my agents:", error);
}
};
loadMyAgents();
}
}, [open, api]);
const handleClose = () => {
setStep("select");
setSelectedAgent(null);
setPublishData({
name: "",
sub_heading: "",
description: "",
image_urls: [],
agent_id: "",
agent_version: 0,
slug: "",
categories: [],
});
setOpen(false);
};
const handleAgentSelect = (agentName: string) => {
setSelectedAgent(agentName);
};
const handleNextFromSelect = (agentId: string, agentVersion: number) => {
const selectedAgentData = myAgents?.agents.find(
(agent) => agent.agent_id === agentId,
);
const name = selectedAgentData?.agent_name || "";
const description = selectedAgentData?.description || "";
setInitialData({
agent_id: agentId,
title: name,
subheader: "",
description: description,
thumbnailSrc: selectedAgentData?.agent_image || "",
youtubeLink: "",
category: "",
slug: name.replace(/ /g, "-"),
additionalImages: [],
});
setStep("info");
setSelectedAgentId(agentId);
setSelectedAgentVersion(agentVersion);
};
const handleNextFromInfo = async (
name: string,
subHeading: string,
slug: string,
description: string,
imageUrls: string[],
videoUrl: string,
categories: string[],
) => {
const missingFields: string[] = [];
if (!name) missingFields.push("Name");
if (!subHeading) missingFields.push("Sub-heading");
if (!description) missingFields.push("Description");
if (!imageUrls.length) missingFields.push("Image");
if (!categories.filter(Boolean).length) missingFields.push("Categories");
if (missingFields.length > 0) {
toast({
title: "Missing Required Fields",
description: `Please fill in: ${missingFields.join(", ")}`,
duration: 3000,
});
return;
}
const filteredCategories = categories.filter(Boolean);
setPublishData({
name,
sub_heading: subHeading,
description,
image_urls: imageUrls,
video_url: videoUrl,
agent_id: selectedAgentId || "",
agent_version: selectedAgentVersion || 0,
slug,
categories: filteredCategories,
});
// Create store submission
try {
await api.createStoreSubmission({
name: name,
sub_heading: subHeading,
description: description,
image_urls: imageUrls,
video_url: videoUrl,
agent_id: selectedAgentId || "",
agent_version: selectedAgentVersion || 0,
slug: slug.replace(/\s+/g, "-"),
categories: filteredCategories,
});
} catch (error) {
console.error("Error creating store submission:", error);
}
setStep("review");
};
const handleBack = () => {
if (step === "info") {
setStep("select");
} else if (step === "review") {
setStep("info");
}
};
const renderContent = () => {
switch (step) {
case "select":
return (
<div className="flex min-h-screen items-center justify-center">
<div className="mx-auto flex w-full max-w-[900px] flex-col rounded-3xl bg-white shadow-lg dark:bg-gray-800">
<div className="h-full overflow-y-auto">
<PublishAgentSelect
agents={
myAgents?.agents
.map((agent) => ({
name: agent.agent_name,
id: agent.agent_id,
version: agent.agent_version,
lastEdited: agent.last_edited,
imageSrc:
agent.agent_image || "https://picsum.photos/300/200",
}))
.sort(
(a, b) =>
new Date(b.lastEdited).getTime() -
new Date(a.lastEdited).getTime(),
) || []
}
onSelect={handleAgentSelect}
onCancel={handleClose}
onNext={handleNextFromSelect}
onClose={handleClose}
onOpenBuilder={() => router.push("/build")}
/>
</div>
</div>
</div>
);
case "info":
return (
<div className="flex min-h-screen items-center justify-center">
<div className="mx-auto flex w-full max-w-[900px] flex-col rounded-3xl bg-white shadow-lg dark:bg-gray-800">
<div className="h-[700px] overflow-y-auto">
<PublishAgentInfo
onBack={handleBack}
onSubmit={handleNextFromInfo}
onClose={handleClose}
initialData={initialData}
/>
</div>
</div>
</div>
);
case "review":
return publishData ? (
<div className="flex justify-center">
<div className="mx-auto flex w-full max-w-[900px] flex-col rounded-3xl bg-white shadow-lg dark:bg-gray-800">
<div className="h-[600px] overflow-y-auto">
<PublishAgentAwaitingReview
agentName={publishData.name}
subheader={publishData.sub_heading}
description={publishData.description}
thumbnailSrc={publishData.image_urls[0]}
onClose={handleClose}
onDone={handleClose}
onViewProgress={() => {
router.push("/profile/dashboard");
handleClose();
}}
/>
</div>
</div>
</div>
) : null;
}
};
return (
<Popover
open={open}
onOpenChange={(isOpen) => {
if (isOpen !== open) {
setOpen(isOpen);
}
}}
>
<PopoverTrigger asChild>
{trigger || <Button>Publish Agent</Button>}
</PopoverTrigger>
<PopoverAnchor asChild>
<div className="fixed left-0 top-0 hidden h-screen w-screen items-center justify-center"></div>
</PopoverAnchor>
<PopoverContent
id={popupId}
align="center"
className="z-50 h-screen w-screen bg-transparent"
>
{renderContent()}
</PopoverContent>
</Popover>
);
};