feat(platform): Updates to agent dashboard (#8598)

* Updates to agent dashboard

* fix squished "select all" text in table

* fix table alignment

---------

Co-authored-by: Swifty <craigswift13@gmail.com>
This commit is contained in:
Bently
2024-11-20 10:01:15 +00:00
committed by GitHub
parent afe5c12afb
commit f6c1bdccac
9 changed files with 371 additions and 172 deletions

View File

@@ -2,6 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";
import { AgentTable } from "./AgentTable";
import { AgentTableRowProps } from "./AgentTableRow";
import { userEvent, within, expect } from "@storybook/test";
import { StatusType } from "./Status";
const meta: Meta<typeof AgentTable> = {
title: "AGPT UI/Agent Table",
@@ -14,34 +15,37 @@ type Story = StoryObj<typeof AgentTable>;
const sampleAgents: AgentTableRowProps[] = [
{
id: "agent-1",
agentName: "Super Coder",
description: "An AI agent that writes clean, efficient code",
imageSrc:
"https://ddz4ak4pa3d19.cloudfront.net/cache/53/b2/53b2bc7d7900f0e1e60bf64ebf38032d.jpg",
dateSubmitted: "2023-05-15",
status: "Active",
status: "approved",
runs: 1500,
rating: 4.8,
onEdit: () => console.log("Edit Super Coder"),
},
{
id: "agent-2",
agentName: "Data Analyzer",
description: "Processes and analyzes large datasets with ease",
imageSrc:
"https://ddz4ak4pa3d19.cloudfront.net/cache/40/f7/40f7bc97c952f8df0f9c88d29defe8d4.jpg",
dateSubmitted: "2023-05-10",
status: "Active",
status: "awaiting_review",
runs: 1200,
rating: 4.5,
onEdit: () => console.log("Edit Data Analyzer"),
},
{
id: "agent-3",
agentName: "UI Designer",
description: "Creates beautiful and intuitive user interfaces",
imageSrc:
"https://ddz4ak4pa3d19.cloudfront.net/cache/14/9e/149ebb9014aa8c0097e72ed89845af0e.jpg",
dateSubmitted: "2023-05-05",
status: "Inactive",
status: "draft",
runs: 800,
rating: 4.2,
onEdit: () => console.log("Edit UI Designer"),

View File

@@ -9,62 +9,56 @@ export interface AgentTableProps {
export const AgentTable: React.FC<AgentTableProps> = ({ agents }) => {
return (
<div className="mx-auto w-full max-w-[1095px]">
{/* Table for medium and larger screens */}
<div className="hidden md:block">
<table className="w-full">
<thead>
<tr className="border-b border-[#d9d9d9]">
<th className="font-['PP Neue Montreal TT'] py-4 text-left text-base leading-[21px] tracking-tight text-[#282828]">
Agent
</th>
<th className="font-['PP Neue Montreal TT'] py-4 text-left text-base leading-[21px] tracking-tight text-[#282828]">
Date submitted
</th>
<th className="font-['PP Neue Montreal TT'] py-4 text-left text-base leading-[21px] tracking-tight text-[#282828]">
Status
</th>
<th className="font-['PP Neue Montreal TT'] py-4 text-left text-base leading-[21px] tracking-tight text-[#282828]">
Runs
</th>
<th className="font-['PP Neue Montreal TT'] py-4 text-left text-base leading-[21px] tracking-tight text-[#282828]">
Reviews
</th>
<th className="font-['PP Neue Montreal TT'] py-4 text-left text-base leading-[21px] tracking-tight text-[#282828]">
Actions
</th>
</tr>
</thead>
<tbody>
{agents.length > 0 ? (
agents.map((agent, index) => (
<AgentTableRow key={index} {...agent} />
))
) : (
<tr>
<td
colSpan={6}
className="font-['PP Neue Montreal TT'] py-4 text-center text-base text-[#282828]"
>
No agents available. Create your first agent to get started!
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Cards for small screens */}
<div className="block md:hidden">
{agents.length > 0 ? (
agents.map((agent, index) => (
<AgentTableCard key={index} {...agent} />
))
) : (
<div className="font-['PP Neue Montreal TT'] py-4 text-center text-base text-[#707070]">
No agents available. Create your first agent to get started!
<div className="w-full">
{/* Table header - Hide on mobile */}
<div className="hidden md:flex flex-col">
<div className="border-t border-neutral-300" />
<div className="flex items-center px-4 py-2">
<div className="flex items-center">
<div className="flex items-center min-w-[120px]">
<input
type="checkbox"
id="selectAllAgents"
aria-label="Select all agents"
className="w-5 h-5 rounded border-2 border-neutral-400 mr-4"
/>
<label
htmlFor="selectAllAgents"
className="text-sm font-medium text-neutral-800"
>
Select all
</label>
</div>
</div>
)}
<div className="grid grid-cols-[400px,150px,150px,100px,100px,50px] w-full items-center ml-2">
<div className="text-sm font-medium text-neutral-800">Agent info</div>
<div className="text-sm font-medium text-neutral-800">Date submitted</div>
<div className="text-sm font-medium text-neutral-800">Status</div>
<div className="text-sm font-medium text-neutral-800 text-right">Runs</div>
<div className="text-sm font-medium text-neutral-800 text-right">Reviews</div>
<div></div>
</div>
</div>
<div className="border-b border-neutral-300" />
</div>
{/* Table body */}
{agents.length > 0 ? (
<div className="flex flex-col">
{agents.map((agent, index) => (
<div key={index} className="md:block">
<AgentTableRow {...agent} />
<div className="block md:hidden">
<AgentTableCard {...agent} />
</div>
</div>
))}
</div>
) : (
<div className="py-4 text-center font-['Geist'] text-base text-neutral-600">
No agents available. Create your first agent to get started!
</div>
)}
</div>
);
};

View File

@@ -1,12 +1,21 @@
// AgentCard.tsx
import * as React from "react";
import Image from "next/image";
import { Button } from "./Button";
import { IconStarFilled, IconEdit } from "@/components/ui/icons";
import { Card } from "@/components/ui/card";
import { AgentTableRowProps } from "./AgentTableRow";
import { IconStarFilled, IconMore } from "@/components/ui/icons";
import { Status, StatusType } from "./Status";
export const AgentTableCard: React.FC<AgentTableRowProps> = ({
export interface AgentTableCardProps {
agentName: string;
description: string;
imageSrc: string;
dateSubmitted: string;
status: StatusType;
runs?: number;
rating?: number;
onEdit: () => void;
}
export const AgentTableCard: React.FC<AgentTableCardProps> = ({
agentName,
description,
imageSrc,
@@ -17,9 +26,9 @@ export const AgentTableCard: React.FC<AgentTableRowProps> = ({
onEdit,
}) => {
return (
<Card className="mb-4 p-4">
<div className="flex">
<div className="relative mr-4 h-20 w-20 overflow-hidden rounded-xl">
<div className="p-4 border-b border-neutral-300">
<div className="flex gap-4">
<div className="relative w-[100px] h-[56px] overflow-hidden rounded-lg bg-[#d9d9d9]">
<Image
src={imageSrc}
alt={agentName}
@@ -28,41 +37,33 @@ export const AgentTableCard: React.FC<AgentTableRowProps> = ({
/>
</div>
<div className="flex-1">
<h3 className="mb-2 font-neue text-lg font-medium tracking-tight text-[#272727]">
{agentName}
</h3>
<p className="mb-2 font-neue text-sm leading-tight tracking-tight text-[#282828]">
{description}
</p>
<div className="mb-2 font-neue text-sm leading-tight tracking-tight text-[#282828]">
<strong>Date submitted:</strong> {dateSubmitted}
</div>
<div className="mb-2 font-neue text-sm leading-tight tracking-tight text-[#282828]">
<strong>Status:</strong> {status}
</div>
{runs !== undefined && (
<div className="mb-2 font-neue text-sm leading-tight tracking-tight text-[#282828]">
<strong>Runs:</strong> {runs.toLocaleString()}
</div>
)}
{rating !== undefined && (
<div className="mb-2 flex items-center font-neue text-sm leading-tight tracking-tight text-[#282828]">
<strong>Rating:</strong>
<span className="ml-2">{rating.toFixed(1)}</span>
<IconStarFilled className="ml-1" />
</div>
)}
<Button
variant="outline"
size="sm"
className="mt-2 flex items-center gap-1"
onClick={onEdit}
>
<IconEdit />
<span>Edit</span>
</Button>
<h3 className="text-[15px] font-medium text-neutral-800">{agentName}</h3>
<p className="text-sm text-neutral-600 line-clamp-2">{description}</p>
</div>
<button onClick={onEdit} className="p-1 hover:bg-neutral-100 rounded-full h-fit">
<IconMore className="h-5 w-5 text-neutral-800" />
</button>
</div>
</Card>
<div className="mt-4 flex flex-wrap gap-4">
<Status status={status} />
<div className="text-sm text-neutral-600">
{dateSubmitted}
</div>
{runs && (
<div className="text-sm text-neutral-600">
{runs.toLocaleString()} runs
</div>
)}
{rating && (
<div className="flex items-center gap-1">
<span className="text-sm font-medium text-neutral-800">
{rating.toFixed(1)}
</span>
<IconStarFilled className="h-4 w-4 text-neutral-800" />
</div>
)}
</div>
</div>
);
};

View File

@@ -1,17 +1,19 @@
import * as React from "react";
import Image from "next/image";
import { Button } from "./Button";
import { IconStarFilled, IconEdit } from "@/components/ui/icons";
import { IconStarFilled, IconEdit, IconMore } from "@/components/ui/icons";
import { Status, StatusType } from "./Status";
export interface AgentTableRowProps {
agentName: string;
description: string;
imageSrc: string;
dateSubmitted: string;
status: string;
status: StatusType;
runs?: number;
rating?: number;
onEdit: () => void;
id: string;
}
export const AgentTableRow: React.FC<AgentTableRowProps> = ({
@@ -23,57 +25,91 @@ export const AgentTableRow: React.FC<AgentTableRowProps> = ({
runs,
rating,
onEdit,
id,
}) => {
// Create a unique ID for the checkbox
const checkboxId = `agent-${id}-checkbox`;
return (
<tr className="border-b border-[#d9d9d9] py-4">
<td className="flex items-center">
<div className="relative my-4 mr-4 h-20 w-20 overflow-hidden rounded-xl sm:h-20 sm:w-[125px]">
<Image
src={imageSrc}
alt={agentName}
layout="fill"
objectFit="cover"
<div className="hidden md:flex items-center px-4 py-4 border-b border-neutral-300 hover:bg-neutral-50">
<div className="flex items-center">
<div className="flex items-center">
<input
type="checkbox"
id={checkboxId}
aria-label={`Select ${agentName}`}
className="w-5 h-5 rounded border-2 border-neutral-400 mr-4"
/>
{/* Single label instead of multiple */}
<label
htmlFor={checkboxId}
className="sr-only"
>
Select {agentName}
</label>
</div>
<div className="max-w-[293px]">
<h3 className="mb-2 font-neue text-lg font-medium tracking-tight text-[#272727]">
{agentName}
</h3>
<p className="font-neue text-sm leading-tight tracking-tight text-[#282828]">
{description}
</p>
</div>
</td>
<td className="font-neue text-base leading-[21px] tracking-tight text-[#282828]">
{dateSubmitted}
</td>
<td className="font-neue text-base leading-[21px] tracking-tight text-[#282828]">
{status}
</td>
<td className="font-neue text-base leading-[21px] tracking-tight text-[#282828]">
{runs !== undefined ? runs.toLocaleString() : ""}
</td>
<td>
{rating !== undefined && (
<div className="flex items-center">
<span className="mr-2 font-neue text-base font-medium tracking-tight text-[#272727]">
{rating.toFixed(1)}
</span>
<IconStarFilled />
</div>
<div className="grid grid-cols-[minmax(400px,1fr),180px,140px,100px,100px,40px] gap-4 w-full items-center">
{/* Agent info column */}
<div className="flex items-center gap-4">
<div className="relative w-[125px] h-[70px] overflow-hidden rounded-[10px] bg-[#d9d9d9]">
<Image
src={imageSrc}
alt={agentName}
layout="fill"
objectFit="cover"
/>
</div>
)}
</td>
<td>
<Button
variant="outline"
size="sm"
className="flex items-center gap-1"
onClick={onEdit}
>
<IconEdit />
<span className="font-neue text-sm">Edit</span>
</Button>
</td>
</tr>
<div className="flex flex-col">
<h3 className="text-[15px] font-medium text-neutral-800">
{agentName}
</h3>
<p className="text-sm text-neutral-600 line-clamp-2">
{description}
</p>
</div>
</div>
{/* Date column */}
<div className="text-sm text-neutral-600 pl-14">
{dateSubmitted}
</div>
{/* Status column */}
<div>
<Status status={status} />
</div>
{/* Runs column */}
<div className="text-sm text-neutral-600 text-right">
{runs?.toLocaleString() ?? '—'}
</div>
{/* Reviews column */}
<div className="text-right">
{rating ? (
<div className="flex items-center justify-end gap-1">
<span className="text-sm font-medium text-neutral-800">
{rating.toFixed(1)}
</span>
<IconStarFilled className="h-4 w-4 text-neutral-800" />
</div>
) : (
<span className="text-sm text-neutral-600"></span>
)}
</div>
{/* Actions - Three dots menu */}
<div className="flex justify-end">
<button
onClick={onEdit}
className="p-1 hover:bg-neutral-100 rounded-full"
>
<IconMore className="h-5 w-5 text-neutral-800" />
</button>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,55 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Status, StatusType } from "./Status";
const meta = {
title: "AGPT UI/Status",
component: Status,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {
status: {
control: 'select',
options: ['draft', 'awaiting_review', 'approved', 'rejected'],
}
}
} satisfies Meta<typeof Status>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Draft: Story = {
args: {
status: "draft" as StatusType,
},
};
export const AwaitingReview: Story = {
args: {
status: "awaiting_review" as StatusType,
},
};
export const Approved: Story = {
args: {
status: "approved" as StatusType,
},
};
export const Rejected: Story = {
args: {
status: "rejected" as StatusType,
},
};
export const AllStatuses: Story = {
render: () => (
<div className="flex flex-col gap-4">
<Status status="draft" />
<Status status="awaiting_review" />
<Status status="approved" />
<Status status="rejected" />
</div>
),
};

View File

@@ -0,0 +1,51 @@
import * as React from "react";
export type StatusType = "draft" | "awaiting_review" | "approved" | "rejected";
interface StatusProps {
status: StatusType;
}
const statusConfig: Record<StatusType, {
bgColor: string;
dotColor: string;
text: string;
}> = {
draft: {
bgColor: "bg-blue-50",
dotColor: "bg-blue-500",
text: "Draft"
},
awaiting_review: {
bgColor: "bg-amber-50",
dotColor: "bg-amber-500",
text: "Awaiting review"
},
approved: {
bgColor: "bg-green-50",
dotColor: "bg-green-500",
text: "Approved"
},
rejected: {
bgColor: "bg-red-50",
dotColor: "bg-red-500",
text: "Rejected"
}
};
export const Status: React.FC<StatusProps> = ({ status }) => {
if (!status || !statusConfig[status]) {
return null;
}
const config = statusConfig[status];
return (
<div className={`px-2.5 py-1 ${config.bgColor} rounded-[26px] flex items-center gap-1.5`}>
<div className={`w-3 h-3 ${config.dotColor} rounded-full`} />
<div className="text-neutral-600 text-sm font-normal font-['Geist'] leading-tight">
{config.text}
</div>
</div>
);
};

View File

@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react";
import { CreatorDashboardPage } from "./CreatorDashboardPage";
import { IconType } from "../../ui/icons";
import { StatusType } from "../Status";
const meta: Meta<typeof CreatorDashboardPage> = {
title: "AGPT UI/Agent Store/Creator Dashboard Page",
@@ -80,7 +81,7 @@ const sampleAgents = [
imageSrc:
"https://ddz4ak4pa3d19.cloudfront.net/cache/11/47/114784105a9b180e08e117cbf2612e5b.jpg",
dateSubmitted: "2023-05-15",
status: "Active",
status: "approved" as StatusType,
runs: 1500,
rating: 4.8,
onEdit: () => console.log("Edit Super Coder"),
@@ -91,11 +92,22 @@ const sampleAgents = [
imageSrc:
"https://ddz4ak4pa3d19.cloudfront.net/cache/40/f7/40f7bc97c952f8df0f9c88d29defe8d4.jpg",
dateSubmitted: "2023-05-10",
status: "Active",
status: "awaiting_review" as StatusType,
runs: 1200,
rating: 4.5,
onEdit: () => console.log("Edit Data Analyzer"),
},
{
agentName: "UI Designer",
description: "Creates beautiful and intuitive user interfaces",
imageSrc:
"https://ddz4ak4pa3d19.cloudfront.net/cache/14/9e/149ebb9014aa8c0097e72ed89845af0e.jpg",
dateSubmitted: "2023-05-05",
status: "draft" as StatusType,
runs: 800,
rating: 4.2,
onEdit: () => console.log("Edit UI Designer"),
},
];
export const Default: Story = {
@@ -126,6 +138,9 @@ export const ManyAgents: Story = {
.map((agent, index) => ({
...agent,
agentName: `Agent ${index + 1}`,
status: ["approved", "awaiting_review", "draft", "rejected"][index % 4] as StatusType,
rating: Math.round((4 + Math.random()) * 10) / 10,
runs: Math.floor(Math.random() * 2000) + 500,
onEdit: () => console.log(`Edit Agent ${index + 1}`),
})),
},

View File

@@ -60,35 +60,43 @@ export const CreatorDashboardPage: React.FC<CreatorDashboardPageProps> = ({
menuItemGroups={menuItemGroups}
/>
<div className="flex">
<div className="flex flex-col md:flex-row">
<Sidebar linkGroups={sidebarLinkGroups} />
<main className="flex-1 px-6 py-8 md:px-10">
<main className="flex-1 px-4 py-6 md:px-10 md:py-8">
{/* Header Section */}
<div className="mb-8 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h1 className="font-neue text-3xl font-medium leading-9 tracking-tight text-neutral-900">
Submit a New Agent
</h1>
<p className="mt-2 font-neue text-sm text-[#707070]">
Select from the list of agents you currently have, or upload
from your local machine.
</p>
</div>
<Button variant="default" size="lg">
Create New Agent
</Button>
<div className="mb-6 md:mb-8">
<h1 className="text-2xl md:text-[32px] font-medium leading-tight md:leading-[38px] text-neutral-900">
Agent dashboard
</h1>
</div>
<Separator className="mb-8" />
{/* Agents Section */}
<div>
<h2 className="mb-4 text-xl font-bold text-neutral-900">
Your Agents
{/* Submit Agent Section */}
<div className="mb-6 md:mb-8">
<h2 className="mb-1 text-lg md:text-[20px] font-medium leading-tight md:leading-[24px] text-neutral-900">
Submit an agent
</h2>
<AgentTable agents={agents} />
<div className="flex flex-col md:flex-row md:justify-between md:items-end gap-4 md:gap-0">
<p className="text-sm md:text-[14px] leading-[20px] text-neutral-600">
Select from the list of agents you currently already have, or upload from your local machine.
</p>
<button className="w-full md:w-auto h-12 px-5 py-3 bg-neutral-800 rounded-[38px] inline-flex items-center justify-center md:justify-start gap-2.5 md:ml-4">
<span className="text-white text-base font-medium font-['Geist'] leading-normal">Submit agent</span>
</button>
</div>
</div>
<Separator className="my-6 md:my-8 bg-neutral-300" />
{/* Agents List Section */}
<div className="mb-4 md:mb-6">
<h2 className="text-lg md:text-[20px] font-medium leading-tight md:leading-[24px] text-neutral-900">
Your uploaded agents
</h2>
</div>
{/* Agent Table */}
<AgentTable agents={agents} />
</main>
</div>
</div>

View File

@@ -1504,6 +1504,41 @@ export const IconSliders = createIcon((props) => (
</svg>
));
/**
* More (vertical dots) icon component.
*
* @component IconMore
* @param {IconProps} props - The props object containing additional attributes and event handlers for the icon.
* @returns {JSX.Element} - The more options icon.
*
* @example
* // Default usage
* <IconMore />
*
* @example
* // With custom color and size
* <IconMore className="text-neutral-800" size="lg" />
*/
export const IconMore = createIcon((props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="currentColor"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="More Icon"
{...props}
>
<path d="M10 10.8333C10.4603 10.8333 10.8334 10.4602 10.8334 9.99999C10.8334 9.53975 10.4603 9.16666 10 9.16666C9.53978 9.16666 9.16669 9.53975 9.16669 9.99999C9.16669 10.4602 9.53978 10.8333 10 10.8333Z" />
<path d="M10 4.99999C10.4603 4.99999 10.8334 4.6269 10.8334 4.16666C10.8334 3.70642 10.4603 3.33333 10 3.33333C9.53978 3.33333 9.16669 3.70642 9.16669 4.16666C9.16669 4.6269 9.53978 4.99999 10 4.99999Z" />
<path d="M10 16.6667C10.4603 16.6667 10.8334 16.2936 10.8334 15.8333C10.8334 15.3731 10.4603 15 10 15C9.53978 15 9.16669 15.3731 9.16669 15.8333C9.16669 16.2936 9.53978 16.6667 10 16.6667Z" />
</svg>
));
/**
* External link icon component.
*