mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-30 03:00:41 -04:00
Reorganize marketplace components and update imports
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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 (
|
||||
@@ -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 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,
|
||||
};
|
||||
};
|
||||
@@ -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]">
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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">
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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">
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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?.();
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
>
|
||||
@@ -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];
|
||||
@@ -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"
|
||||
@@ -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>
|
||||
@@ -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 (
|
||||
@@ -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]">
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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">
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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">
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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">
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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"
|
||||
@@ -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">
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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">
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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">
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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">
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user