fix(market): Market featured agent card (#9463)

### Background
Resolves: #9313

The marketplace featured agent's section has a bug where if you hover
over a featured agent's card we are getting an incorrect background
color applied to the description.

### Changes 🏗️

1. Refactored `FeaturedStoreCard` to `FeaturedAgentCard`:
   - Condensed props and leverage StoreAgent type from api
- Removed onClick handler from props as this is not json serializable
and is not inline with NextJS best practices
- Used built in Card Components from ShadCN to minimize custom styling.
- Optimize images with implementation of the Image component from NextJS

2. Enhanced `FeaturedCardSection` components:
- Removing extensive prop passing and leverage the agent itself with the
StoreAgent type.
- Implemented Link from NextJS to better handler routing and remove the
`useRouter` implementation
   - Removed unnecessary handleCardClick method.

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:

Test Plan
<details>
  <summary></summary>
 
  - [ ] Goto the landing page of the application or /marketplace 
  - [ ] Scroll to the featured agents section
- [ ] Move mouse over each of the cards and observe the image
disappearing and text being shown
- [ ] Observe the background color of the text that replaced the image
matches that of the card
</details>
This commit is contained in:
Andy Hooker
2025-02-10 16:01:29 -06:00
committed by GitHub
parent 64050faef6
commit 6eee9206f7
6 changed files with 141 additions and 246 deletions

View File

@@ -1,9 +1,6 @@
import * as React from "react";
import { HeroSection } from "@/components/agptui/composite/HeroSection";
import {
FeaturedSection,
FeaturedAgent,
} from "@/components/agptui/composite/FeaturedSection";
import { FeaturedSection } from "@/components/agptui/composite/FeaturedSection";
import {
AgentsSection,
Agent,
@@ -155,9 +152,7 @@ export default async function Page({}: {}) {
<div className="mx-auto w-screen max-w-[1360px]">
<main className="px-4">
<HeroSection />
<FeaturedSection
featuredAgents={featuredAgents.agents as FeaturedAgent[]}
/>
<FeaturedSection featuredAgents={featuredAgents.agents} />
<Separator />
<AgentsSection
sectionTitle="Top Agents"

View File

@@ -0,0 +1,77 @@
import Image from "next/image";
import { StarRatingIcons } from "@/components/ui/icons";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useState } from "react";
import { StoreAgent } from "@/lib/autogpt-server-api";
interface FeaturedStoreCardProps {
agent: StoreAgent;
backgroundColor: string;
}
export const FeaturedAgentCard: React.FC<FeaturedStoreCardProps> = ({
agent,
backgroundColor,
}) => {
const [isHovered, setIsHovered] = useState(false);
return (
<Card
data-testid="featured-store-card"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={backgroundColor}
>
<CardHeader>
<CardTitle>{agent.agent_name}</CardTitle>
<CardDescription>{agent.description}</CardDescription>
</CardHeader>
<CardContent>
<div className="relative h-[397px] w-full overflow-hidden rounded-xl">
<div
className={`transition-opacity duration-200 ${isHovered ? "opacity-0" : "opacity-100"}`}
>
<Image
src={agent.agent_image || "/AUTOgpt_Logo_dark.png"}
alt={`${agent.agent_name} preview`}
fill
sizes="100%"
className="rounded-xl object-cover"
/>
</div>
<div
className={`absolute inset-0 overflow-y-auto transition-opacity duration-200 ${
isHovered ? "opacity-100" : "opacity-0"
} rounded-xl dark:bg-neutral-700`}
>
<p className="text-base text-neutral-800 dark:text-neutral-200">
{agent.description}
</p>
</div>
</div>
</CardContent>
<CardFooter className="flex items-center justify-between">
<div className="font-semibold">
{agent.runs?.toLocaleString() ?? "0"} runs
</div>
<div className="flex items-center gap-1.5">
<p>{agent.rating.toFixed(1) ?? "0.0"}</p>
<div
className="inline-flex items-center justify-start gap-px"
role="img"
aria-label={`Rating: ${agent.rating.toFixed(1)} out of 5 stars`}
>
{StarRatingIcons(agent.rating)}
</div>
</div>
</CardFooter>
</Card>
);
};

View File

@@ -1,10 +1,10 @@
import type { Meta, StoryObj } from "@storybook/react";
import { FeaturedStoreCard } from "./FeaturedStoreCard";
import { FeaturedAgentCard } from "./FeaturedAgentCard";
import { userEvent, within } from "@storybook/test";
const meta = {
title: "AGPT UI/Featured Store Card",
component: FeaturedStoreCard,
component: FeaturedAgentCard,
parameters: {
layout: {
center: true,
@@ -13,123 +13,63 @@ const meta = {
},
tags: ["autodocs"],
argTypes: {
agentName: { control: "text" },
subHeading: { control: "text" },
agentImage: { control: "text" },
creatorImage: { control: "text" },
creatorName: { control: "text" },
description: { control: "text" },
runs: { control: "number" },
rating: { control: "number", min: 0, max: 5, step: 0.1 },
onClick: { action: "clicked" },
agent: {
agent_name: { control: "text" },
sub_heading: { control: "text" },
agent_image: { control: "text" },
creator_avatar: { control: "text" },
creator: { control: "text" },
runs: { control: "number" },
rating: { control: "number", min: 0, max: 5, step: 0.1 },
slug: { control: "text" },
},
backgroundColor: {
control: "color",
},
},
} satisfies Meta<typeof FeaturedStoreCard>;
} satisfies Meta<typeof FeaturedAgentCard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
agentName: "Personalized Morning Coffee Newsletter example of three lines",
subHeading:
"Transform ideas into breathtaking images with this AI-powered Image Generator.",
description:
"Elevate your web content with this powerful AI Webpage Copy Improver. Designed for marketers, SEO specialists, and web developers, this tool analyses and enhances website copy for maximum impact. Using advanced language models, it optimizes text for better clarity, SEO performance, and increased conversion rates.",
agentImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorName: "AI Solutions Inc.",
runs: 50000,
rating: 4.7,
onClick: () => console.log("Card clicked"),
backgroundColor: "bg-white",
},
};
export const LowRating: Story = {
args: {
agentName: "Data Analyzer Lite",
subHeading: "Basic data analysis tool",
description:
"A lightweight data analysis tool for basic data processing needs.",
agentImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorName: "DataTech",
runs: 10000,
rating: 2.8,
onClick: () => console.log("Card clicked"),
backgroundColor: "bg-white",
},
};
export const HighRuns: Story = {
args: {
agentName: "CodeAssist AI",
subHeading: "Your AI coding companion",
description:
"An intelligent coding assistant that helps developers write better code faster.",
agentImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorName: "DevTools Co.",
runs: 1000000,
rating: 4.9,
onClick: () => console.log("Card clicked"),
backgroundColor: "bg-white",
},
};
export const NoCreatorImage: Story = {
args: {
agentName: "MultiTasker",
subHeading: "All-in-one productivity suite",
description:
"A comprehensive productivity suite that combines task management, note-taking, and project planning into one seamless interface.",
agentImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorName: "Productivity Plus",
runs: 75000,
rating: 4.5,
onClick: () => console.log("Card clicked"),
backgroundColor: "bg-white",
},
};
export const ShortDescription: Story = {
args: {
agentName: "QuickTask",
subHeading: "Fast task automation",
description: "Simple and efficient task automation tool.",
agentImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorName: "EfficientWorks",
runs: 50000,
rating: 4.2,
onClick: () => console.log("Card clicked"),
agent: {
agent_name:
"Personalized Morning Coffee Newsletter example of three lines",
sub_heading:
"Transform ideas into breathtaking images with this AI-powered Image Generator.",
description:
"Elevate your web content with this powerful AI Webpage Copy Improver. Designed for marketers, SEO specialists, and web developers, this tool analyses and enhances website copy for maximum impact. Using advanced language models, it optimizes text for better clarity, SEO performance, and increased conversion rates.",
agent_image:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creator_avatar:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creator: "AI Solutions Inc.",
runs: 50000,
rating: 4.7,
slug: "",
},
backgroundColor: "bg-white",
},
};
export const WithInteraction: Story = {
args: {
agentName: "AI Writing Assistant",
subHeading: "Enhance your writing",
description:
"An AI-powered writing assistant that helps improve your writing style and clarity.",
agentImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creatorName: "WordCraft AI",
runs: 200000,
rating: 4.6,
onClick: () => console.log("Card clicked"),
agent: {
slug: "",
agent_name: "AI Writing Assistant",
sub_heading: "Enhance your writing",
description:
"An AI-powered writing assistant that helps improve your writing style and clarity.",
agent_image:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creator_avatar:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
creator: "WordCraft AI",
runs: 200000,
rating: 4.6,
},
backgroundColor: "bg-white",
},
play: async ({ canvasElement }) => {

View File

@@ -1,96 +0,0 @@
import * as React from "react";
import Image from "next/image";
import { StarRatingIcons } from "@/components/ui/icons";
interface FeaturedStoreCardProps {
agentName: string;
subHeading: string;
agentImage: string;
creatorImage?: string;
creatorName: string;
description: string; // Added description prop
runs: number;
rating: number;
onClick: () => void;
backgroundColor: string;
}
export const FeaturedStoreCard: React.FC<FeaturedStoreCardProps> = ({
agentName,
subHeading,
agentImage,
creatorImage,
creatorName,
description,
runs,
rating,
onClick,
backgroundColor,
}) => {
return (
<div
className={`group h-[755px] w-[440px] px-[22px] pb-5 pt-[30px] ${backgroundColor} inline-flex flex-col items-start justify-start gap-7 rounded-[26px] transition-all duration-200 hover:brightness-95 dark:bg-neutral-800`}
onClick={onClick}
data-testid="featured-store-card"
>
<div className="flex h-[188px] flex-col items-start justify-start gap-3 self-stretch">
<h2 className="font-poppins self-stretch text-[35px] font-medium leading-10 text-neutral-900 dark:text-neutral-100">
{agentName}
</h2>
<div className="font-lead self-stretch text-xl font-normal leading-7 text-neutral-800 dark:text-neutral-200">
{subHeading}
</div>
</div>
<div className="flex h-[489px] flex-col items-start justify-start gap-[18px] self-stretch">
<div className="font-lead self-stretch text-xl font-normal leading-7 text-neutral-800 dark:text-neutral-200">
by {creatorName}
</div>
<div className="relative h-[397px] self-stretch">
<Image
src={agentImage}
alt={`${agentName} preview`}
layout="fill"
objectFit="cover"
className="rounded-xl transition-opacity duration-200 group-hover:opacity-0"
/>
<div className="absolute inset-0 overflow-y-auto rounded-xl bg-white p-4 opacity-0 transition-opacity duration-200 group-hover:opacity-100 dark:bg-neutral-700">
<div className="font-geist text-base font-normal leading-normal text-neutral-800 dark:text-neutral-200">
{description}
</div>
</div>
{creatorImage && (
<div className="absolute left-[8.74px] top-[313px] h-[74px] w-[74px] overflow-hidden rounded-full transition-opacity duration-200 group-hover:opacity-0">
<Image
src={creatorImage}
alt={`${creatorName} image`}
layout="fill"
className="object-cover"
priority
/>
</div>
)}
</div>
<div className="inline-flex items-center justify-between self-stretch">
<div className="font-large-geist text-lg font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
{runs.toLocaleString()} runs
</div>
<div className="flex items-center justify-start gap-[5px]">
<div className="font-large-geist text-lg font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
{rating.toFixed(1)}
</div>
<div
className="inline-flex items-center justify-start gap-px"
role="img"
aria-label={`Rating: ${rating.toFixed(1)} out of 5 stars`}
>
{StarRatingIcons(rating)}
</div>
</div>
</div>
</div>
</div>
);
};

View File

@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react";
import { FeaturedAgent, FeaturedSection } from "./FeaturedSection";
import { userEvent, within, expect } from "@storybook/test";
import { FeaturedSection } from "./FeaturedSection";
import { userEvent, within } from "@storybook/test";
import { StoreAgent } from "@/lib/autogpt-server-api";
const meta = {
title: "AGPT UI/Composite/Featured Agents",
@@ -15,7 +16,6 @@ const meta = {
tags: ["autodocs"],
argTypes: {
featuredAgents: { control: "object" },
// onCardClick: { action: "clicked" },
},
} satisfies Meta<typeof FeaturedSection>;
@@ -93,7 +93,7 @@ const mockFeaturedAgents = [
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
slug: "quicktask",
},
] satisfies FeaturedAgent[];
] satisfies StoreAgent[];
export const Default: Story = {
args: {

View File

@@ -1,7 +1,7 @@
"use client";
import * as React from "react";
import { FeaturedStoreCard } from "@/components/agptui/FeaturedStoreCard";
import { FeaturedAgentCard } from "@/components/agptui/FeaturedAgentCard";
import {
Carousel,
CarouselContent,
@@ -11,7 +11,8 @@ import {
CarouselIndicator,
} from "@/components/ui/carousel";
import { useCallback, useState } from "react";
import { useRouter } from "next/navigation";
import { StoreAgent } from "@/lib/autogpt-server-api";
import Link from "next/link";
const BACKGROUND_COLORS = [
"bg-violet-200 dark:bg-violet-800", // #ddd6fe / #5b21b6
@@ -19,33 +20,14 @@ const BACKGROUND_COLORS = [
"bg-green-200 dark:bg-green-800", // #bbf7d0 / #065f46
];
export interface FeaturedAgent {
slug: string;
agent_name: string;
agent_image: string;
creator: string;
creator_avatar: string;
sub_heading: string;
description: string;
runs: number;
rating: number;
}
interface FeaturedSectionProps {
featuredAgents: FeaturedAgent[];
featuredAgents: StoreAgent[];
}
export const FeaturedSection: React.FC<FeaturedSectionProps> = ({
featuredAgents,
}) => {
const [currentSlide, setCurrentSlide] = useState(0);
const router = useRouter();
const handleCardClick = (creator: string, slug: string) => {
router.push(
`/marketplace/agent/${encodeURIComponent(creator)}/${encodeURIComponent(slug)}`,
);
};
const handlePrevSlide = useCallback(() => {
setCurrentSlide((prev) =>
@@ -84,17 +66,14 @@ export const FeaturedSection: React.FC<FeaturedSectionProps> = ({
key={index}
className="max-w-[460px] flex-[0_0_auto]"
>
<FeaturedStoreCard
agentName={agent.agent_name}
subHeading={agent.sub_heading}
agentImage={agent.agent_image}
creatorName={agent.creator}
description={agent.description}
runs={agent.runs}
rating={agent.rating}
backgroundColor={getBackgroundColor(index)}
onClick={() => handleCardClick(agent.creator, agent.slug)}
/>
<Link
href={`/marketplace/agent/${encodeURIComponent(agent.creator)}/${encodeURIComponent(agent.slug)}`}
>
<FeaturedAgentCard
agent={agent}
backgroundColor={getBackgroundColor(index)}
/>
</Link>
</CarouselItem>
))}
</CarouselContent>