mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-12 08:38:09 -05:00
Compare commits
72 Commits
gitbook
...
abhi/ci-ch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
feac4b7579 | ||
|
|
90101604a7 | ||
|
|
20d5845c72 | ||
|
|
740a20fc87 | ||
|
|
cc16ac103a | ||
|
|
b1528c28db | ||
|
|
7c511ea0d3 | ||
|
|
a922f73f3e | ||
|
|
6425612e0b | ||
|
|
c6cc00f9a7 | ||
|
|
7449a378e5 | ||
|
|
4fa44df262 | ||
|
|
76db88730f | ||
|
|
8eedfade11 | ||
|
|
fe472e2dfb | ||
|
|
d93b6ecf59 | ||
|
|
1acc000593 | ||
|
|
fd345f2a3c | ||
|
|
249d61391e | ||
|
|
212c105acc | ||
|
|
5e7958ce60 | ||
|
|
ac9b0ad263 | ||
|
|
eadb3e88f9 | ||
|
|
5d198e4fad | ||
|
|
86cbd6edd1 | ||
|
|
781d8eb8b4 | ||
|
|
368df7bd3b | ||
|
|
5ba94958d9 | ||
|
|
2d5f541743 | ||
|
|
234ce73868 | ||
|
|
a7926e1249 | ||
|
|
bb7c8ba419 | ||
|
|
3c2fb6a3c5 | ||
|
|
b499b6a896 | ||
|
|
4d6ef9fcd1 | ||
|
|
cf015e80e3 | ||
|
|
dc16964a81 | ||
|
|
f4e0735d22 | ||
|
|
36578e29c3 | ||
|
|
a6fcb3abcd | ||
|
|
653a7f475a | ||
|
|
50b2fc30a5 | ||
|
|
c26b5dc611 | ||
|
|
43a508d9a0 | ||
|
|
94f0f5177a | ||
|
|
b3d4d81f76 | ||
|
|
141384856d | ||
|
|
35da036355 | ||
|
|
36bda1cd34 | ||
|
|
01557a6a68 | ||
|
|
073f9f4d58 | ||
|
|
6b44ca536c | ||
|
|
4ef5c5ca20 | ||
|
|
f60d44ecb9 | ||
|
|
c00d7a9f6d | ||
|
|
ee4ebc1956 | ||
|
|
bdbf8f2ffd | ||
|
|
0b224568e6 | ||
|
|
d76e2ac1f6 | ||
|
|
5679d6eb7f | ||
|
|
dfa29b9b22 | ||
|
|
fda57a423a | ||
|
|
dc7f482d0a | ||
|
|
4fd11676f2 | ||
|
|
335e4a695f | ||
|
|
34345dbd47 | ||
|
|
5e73bcc812 | ||
|
|
a7d4de66c2 | ||
|
|
3989bdd175 | ||
|
|
f341965e52 | ||
|
|
fcf6f3b393 | ||
|
|
52af83cf36 |
24
.github/workflows/platform-frontend-ci.yml
vendored
24
.github/workflows/platform-frontend-ci.yml
vendored
@@ -56,6 +56,30 @@ jobs:
|
||||
run: |
|
||||
yarn type-check
|
||||
|
||||
design:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "21"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
yarn install --frozen-lockfile
|
||||
|
||||
- name: Run Chromatic
|
||||
uses: chromaui/action@latest
|
||||
with:
|
||||
# ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret
|
||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
workingDir: autogpt_platform/frontend
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
|
||||
@@ -9,6 +9,10 @@ const config: StorybookConfig = {
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-interactions",
|
||||
],
|
||||
env: {
|
||||
NEXT_PUBLIC_SUPABASE_URL: "https://your-project.supabase.co",
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: "your-anon-key",
|
||||
},
|
||||
features: {
|
||||
experimentalRSC: true,
|
||||
},
|
||||
@@ -16,6 +20,16 @@ const config: StorybookConfig = {
|
||||
name: "@storybook/nextjs",
|
||||
options: {},
|
||||
},
|
||||
staticDirs: ["../public"],
|
||||
staticDirs: [
|
||||
"../public",
|
||||
{
|
||||
from: "../node_modules/geist/dist/fonts/geist-sans",
|
||||
to: "/fonts/geist-sans",
|
||||
},
|
||||
{
|
||||
from: "../node_modules/geist/dist/fonts/geist-mono",
|
||||
to: "/fonts/geist-mono",
|
||||
},
|
||||
],
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { Preview } from "@storybook/react";
|
||||
import { initialize, mswLoader } from "msw-storybook-addon";
|
||||
import "../src/app/globals.css";
|
||||
|
||||
// Initialize MSW
|
||||
initialize();
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
loaders: [mswLoader],
|
||||
};
|
||||
|
||||
export default preview;
|
||||
61
autogpt_platform/frontend/.storybook/preview.tsx
Normal file
61
autogpt_platform/frontend/.storybook/preview.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from "react";
|
||||
import type { Preview } from "@storybook/react";
|
||||
import { initialize, mswLoader } from "msw-storybook-addon";
|
||||
import { Inter, Poppins } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
import "../src/app/globals.css";
|
||||
import { Providers } from "../src/app/providers";
|
||||
|
||||
const poppins = Poppins({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600", "700"],
|
||||
variable: "--font-poppins",
|
||||
});
|
||||
|
||||
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
|
||||
|
||||
const GeistSans = localFont({
|
||||
src: "../fonts/geist-sans/Geist-Variable.woff2",
|
||||
variable: "--font-geist-sans",
|
||||
});
|
||||
|
||||
const GeistMono = localFont({
|
||||
src: "../fonts/geist-mono/GeistMono-Variable.woff2",
|
||||
variable: "--font-geist-mono",
|
||||
});
|
||||
|
||||
// Initialize MSW
|
||||
initialize();
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
layout: "fullscreen",
|
||||
},
|
||||
decorators: [
|
||||
(Story, context) => {
|
||||
const mockOptions = context.parameters.mockBackend || {};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${poppins.variable} ${inter.variable} ${GeistMono.variable} ${GeistSans.variable}`}
|
||||
>
|
||||
<Providers mockClientProps={mockOptions}>
|
||||
<Story />
|
||||
</Providers>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
],
|
||||
loaders: [mswLoader],
|
||||
};
|
||||
|
||||
export default preview;
|
||||
60
autogpt_platform/frontend/.storybook/stoybook_guide.md
Normal file
60
autogpt_platform/frontend/.storybook/stoybook_guide.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# For Client-Side Components
|
||||
|
||||
When communicating with the server in client components, use the `useBackendAPI` hook. It automatically detects when running in a Storybook environment and switches to the mock client.
|
||||
|
||||
To provide custom mock data instead of the default values, add the `mockBackend` parameter in your stories:
|
||||
|
||||
```tsx
|
||||
export const MyStory = {
|
||||
parameters: {
|
||||
mockBackend: {
|
||||
credits: 100,
|
||||
isAuthenticated: true,
|
||||
// Other custom mock data
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
# For Server-Side Components
|
||||
|
||||
The server-based Supabase client automatically switches between real requests and mock responses.
|
||||
|
||||
For server-side components, use the following pattern to select between backend client and mock client:
|
||||
|
||||
```tsx
|
||||
const api = process.env.STORYBOOK ? new MockClient() : new BackendAPI();
|
||||
```
|
||||
|
||||
You need to override specific API request methods in your mock client implementation. If you don't override a method, you can use the default methods provided by `BackendAPI` in both server-side and client-side environments.
|
||||
|
||||
To use custom mock data in server components, pass it directly to the `MockClient` constructor:
|
||||
|
||||
```tsx
|
||||
const api = process.env.STORYBOOK
|
||||
? new MockClient({ credits: 200, isAuthenticated: true })
|
||||
: new BackendAPI();
|
||||
```
|
||||
|
||||
> Note: For client components, always use the `mockBackend` parameter in stories instead.
|
||||
|
||||
# Using MSW
|
||||
|
||||
If you haven't overridden an API request method in your mock client, you can use Mock Service Worker (MSW) to intercept HTTP requests from both the browser and Node.js environments, then respond with custom mock data:
|
||||
|
||||
```tsx
|
||||
// In your story
|
||||
export const WithMSW = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
http.get("/api/user", () => {
|
||||
return HttpResponse.json({ name: "John", role: "admin" });
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Currently, it doesn't have support for client-side Supabase client and custom data for Supabase server-side client. You could use MSW for both cases.
|
||||
BIN
autogpt_platform/frontend/public/agpt-logo.png
Normal file
BIN
autogpt_platform/frontend/public/agpt-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
37
autogpt_platform/frontend/public/agpt-logo.svg
Normal file
37
autogpt_platform/frontend/public/agpt-logo.svg
Normal file
@@ -0,0 +1,37 @@
|
||||
<svg width="90" height="40" viewBox="0 0 90 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3_4463)">
|
||||
<path d="M69.9139 28.8681V38.6414C69.9139 39.3617 69.3179 39.951 68.5928 39.951C67.8081 39.951 67.1592 39.4599 67.1592 38.6414V24.0584C67.1592 20.9644 69.6987 18.4531 72.8275 18.4531C75.9563 18.4531 78.4958 20.9644 78.4958 24.0584C78.4958 27.1525 75.9563 29.6637 72.8275 29.6637C71.7614 29.6637 70.7681 29.3723 69.9172 28.8681H69.9139ZM71.076 22.3231C72.0428 22.3231 72.8275 23.0991 72.8275 24.0552C72.8275 25.0112 72.0428 25.7872 71.076 25.7872C70.8972 25.7872 70.7217 25.761 70.5595 25.7119C71.0727 26.3994 71.8971 26.8447 72.8275 26.8447C74.3836 26.8447 75.6451 25.5973 75.6451 24.0584C75.6451 22.5196 74.3836 21.2721 72.8275 21.2721C71.8971 21.2721 71.0727 21.7174 70.5595 22.405C70.7217 22.3559 70.8972 22.3297 71.076 22.3297V22.3231Z" fill="url(#paint0_linear_3_4463)"/>
|
||||
<path d="M62.8318 28.8675V35.144C62.8318 35.7137 62.5968 36.2343 62.216 36.6075C61.3817 37.4326 59.8355 37.4326 59.0011 36.6075C57.9052 35.5238 58.9448 33.6903 56.9848 31.752C55.0247 29.8137 51.7734 29.8694 49.8729 31.752C48.9955 32.6196 48.4492 33.8212 48.4492 35.144C48.4492 35.8643 49.0452 36.4536 49.7736 36.4536C50.5616 36.4536 51.2072 35.9625 51.2072 35.144C51.2072 34.5743 51.4423 34.057 51.823 33.6805C52.6574 32.8554 54.2036 32.8554 55.0379 33.6805C56.2332 34.8624 55.0247 36.5289 57.0543 38.536C58.9581 40.4186 62.2657 40.4186 64.1661 38.536C65.0435 37.6683 65.5898 36.4667 65.5898 35.144V24.0545C65.5898 20.9605 63.0504 18.4492 59.9215 18.4492C56.7927 18.4492 54.2533 20.9605 54.2533 24.0545C54.2533 27.1486 56.7927 29.6598 59.9215 29.6598C60.9877 29.6598 61.9842 29.3684 62.8318 28.8642V28.8675ZM59.9215 26.8441C58.9912 26.8441 58.1668 26.3988 57.6536 25.7112C57.8158 25.7603 57.9913 25.7865 58.1701 25.7865C59.1369 25.7865 59.9215 25.0106 59.9215 24.0545C59.9215 23.0985 59.1369 22.3225 58.1701 22.3225C57.9913 22.3225 57.8158 22.3487 57.6536 22.3978C58.1668 21.7103 58.9912 21.265 59.9215 21.265C61.4777 21.265 62.7391 22.5124 62.7391 24.0512C62.7391 25.5901 61.4777 26.8375 59.9215 26.8375V26.8441Z" fill="url(#paint1_linear_3_4463)"/>
|
||||
<path d="M82.6281 12.959C82.6281 9.51134 81.2409 6.24048 78.7809 3.80453C76.3175 1.36858 73.0099 0 69.5235 0C66.0372 0 62.7296 1.37186 60.2662 3.80453C57.8029 6.24048 56.4189 9.51461 56.4189 12.959V13.5451C56.4189 14.2948 57.0348 14.9038 57.793 14.9038C58.5512 14.9038 59.167 14.2948 59.167 13.5451V12.959C59.167 10.2349 60.2662 7.64836 62.2097 5.72645C64.1532 3.80453 66.7689 2.71425 69.5235 2.71425C72.2782 2.71425 74.8905 3.80126 76.8374 5.72645C78.7809 7.64836 79.8834 10.2349 79.8834 12.959C79.8834 13.7088 80.4992 14.3178 81.2574 14.3178C82.0156 14.3178 82.6315 13.7088 82.6315 12.959H82.6281Z" fill="url(#paint2_linear_3_4463)"/>
|
||||
<path d="M82.6289 17.061V18.7341H84.8406C85.5756 18.7341 86.1782 19.33 86.1782 20.0569C86.1782 20.7837 85.6485 21.4582 84.8406 21.4582H82.6289V35.1964C82.6289 35.7661 82.864 36.2834 83.2447 36.6599C84.0791 37.485 85.6253 37.485 86.4596 36.6599C86.8404 36.2834 87.0755 35.7661 87.0755 35.1964V34.738C87.0755 33.9228 87.7244 33.4284 88.5091 33.4284C89.2375 33.4284 89.8335 34.0177 89.8335 34.738V35.1964C89.8335 36.5192 89.2872 37.7208 88.4098 38.5884C86.506 40.471 83.2083 40.471 81.3046 38.5884C80.4272 37.7208 79.8809 36.5192 79.8809 35.1964V17.061C79.8809 16.272 80.5132 15.7383 81.288 15.7383C82.0628 15.7383 82.6256 16.3342 82.6256 17.061H82.6289Z" fill="url(#paint3_linear_3_4463)"/>
|
||||
<path d="M76.2765 38.6377C76.2765 39.358 75.6806 39.9441 74.9522 39.9441C74.1675 39.9441 73.5186 39.453 73.5186 38.6377V34.2013C73.5186 33.4809 74.1145 32.8916 74.8429 32.8916C75.6276 32.8916 76.2765 33.3827 76.2765 34.2013V38.6377Z" fill="url(#paint4_linear_3_4463)"/>
|
||||
<path d="M11.9004 22.2907V31.6252H9.04308V26.9399H2.8583V31.6252H0.000976562V22.2907C0.000976562 14.5998 11.9004 14.4983 11.9004 22.2907ZM44.8804 31.6252C49.1085 31.6252 52.5353 28.2365 52.5353 24.0554C52.5353 19.8744 49.1085 16.4857 44.8804 16.4857C40.6524 16.4857 37.2256 19.8744 37.2256 24.0554C37.2256 28.2365 40.6524 31.6252 44.8804 31.6252ZM44.8804 28.7309C42.2714 28.7309 40.1557 26.6387 40.1557 24.0587C40.1557 21.4787 42.2714 19.3865 44.8804 19.3865C47.4894 19.3865 49.6051 21.4787 49.6051 24.0587C49.6051 26.6387 47.4894 28.7309 44.8804 28.7309ZM37.7421 16.4857V19.2097H33.5836V31.6252H30.8289V19.2097H26.6704V16.4857H37.7421ZM25.2931 25.8202V16.4857H22.4357V25.8202C22.4357 30.0242 16.2476 29.9489 16.2476 25.8202V16.4857H13.3903V25.8202C13.3903 33.5111 25.2897 33.6126 25.2897 25.8202H25.2931ZM9.04308 24.2159V22.294C9.04308 18.09 2.85498 18.1653 2.85498 22.294V24.2159H9.04308Z" fill="#000030"/>
|
||||
<path d="M88.4557 32.257C89.2365 32.257 89.8695 31.6311 89.8695 30.859C89.8695 30.0869 89.2365 29.4609 88.4557 29.4609C87.675 29.4609 87.042 30.0869 87.042 30.859C87.042 31.6311 87.675 32.257 88.4557 32.257Z" fill="#669CF6"/>
|
||||
<path d="M49.7702 39.9475C50.551 39.9475 51.184 39.3215 51.184 38.5494C51.184 37.7773 50.551 37.1514 49.7702 37.1514C48.9894 37.1514 48.3564 37.7773 48.3564 38.5494C48.3564 39.3215 48.9894 39.9475 49.7702 39.9475Z" fill="#669CF6"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3_4463" x1="63.4383" y1="20.9589" x2="63.4383" y2="33.2932" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#000030"/>
|
||||
<stop offset="1" stop-color="#9900FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_3_4463" x1="48.0684" y1="20.947" x2="48.0684" y2="33.2951" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#000030"/>
|
||||
<stop offset="1" stop-color="#4285F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_3_4463" x1="70.1948" y1="6.17402" x2="48.7204" y2="-4.1259" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#4285F4"/>
|
||||
<stop offset="1" stop-color="#9900FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_3_4463" x1="75.134" y1="15.7136" x2="75.134" y2="34.5465" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#000030"/>
|
||||
<stop offset="1" stop-color="#4285F4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_3_4463" x1="65.0808" y1="24.1914" x2="65.8035" y2="30.9774" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#4285F4"/>
|
||||
<stop offset="1" stop-color="#9900FF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_3_4463">
|
||||
<rect width="89.8681" height="40" fill="white" transform="translate(0.000976562)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
BIN
autogpt_platform/frontend/public/testing_agent_image.jpg
Normal file
BIN
autogpt_platform/frontend/public/testing_agent_image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 296 KiB |
BIN
autogpt_platform/frontend/public/testing_avatar.png
Normal file
BIN
autogpt_platform/frontend/public/testing_avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@@ -7,14 +7,15 @@ export default function PlatformLayout({ children }: { children: ReactNode }) {
|
||||
<>
|
||||
<Navbar
|
||||
links={[
|
||||
{
|
||||
name: "Home",
|
||||
href: "/library",
|
||||
},
|
||||
{
|
||||
name: "Marketplace",
|
||||
href: "/marketplace",
|
||||
},
|
||||
{
|
||||
name: "Library",
|
||||
href: "/library",
|
||||
},
|
||||
|
||||
{
|
||||
name: "Build",
|
||||
href: "/build",
|
||||
@@ -62,7 +63,7 @@ export default function PlatformLayout({ children }: { children: ReactNode }) {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<main>{children}</main>
|
||||
<main className="w-full">{children}</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,62 +56,49 @@ export default async function Page({
|
||||
|
||||
const breadcrumbs = [
|
||||
{ name: "Marketplace", link: "/marketplace" },
|
||||
{
|
||||
name: agent.creator,
|
||||
link: `/marketplace/creator/${encodeURIComponent(agent.creator)}`,
|
||||
},
|
||||
{ name: agent.agent_name, link: "#" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-screen max-w-[1360px]">
|
||||
<main className="mt-5 px-4">
|
||||
<BreadCrumbs items={breadcrumbs} />
|
||||
|
||||
<div className="mt-4 flex flex-col items-start gap-4 sm:mt-6 sm:gap-6 md:mt-8 md:flex-row md:gap-8">
|
||||
<div className="w-full md:w-auto md:shrink-0">
|
||||
<AgentInfo
|
||||
user={user}
|
||||
name={agent.agent_name}
|
||||
creator={agent.creator}
|
||||
shortDescription={agent.sub_heading}
|
||||
longDescription={agent.description}
|
||||
rating={agent.rating}
|
||||
runs={agent.runs}
|
||||
categories={agent.categories}
|
||||
lastUpdated={agent.updated_at}
|
||||
version={agent.versions[agent.versions.length - 1]}
|
||||
storeListingVersionId={agent.store_listing_version_id}
|
||||
libraryAgent={libraryAgent}
|
||||
/>
|
||||
</div>
|
||||
<AgentImages
|
||||
images={
|
||||
agent.agent_video
|
||||
? [agent.agent_video, ...agent.agent_image]
|
||||
: agent.agent_image
|
||||
}
|
||||
<main className="mt-9 px-10">
|
||||
<BreadCrumbs items={breadcrumbs} />
|
||||
<div className="mt-4 flex flex-col items-start gap-4 sm:mt-6 sm:gap-6 md:mt-8 md:flex-row md:gap-8">
|
||||
<div className="w-full md:w-auto md:shrink-0">
|
||||
<AgentInfo
|
||||
user={user}
|
||||
name={agent.agent_name}
|
||||
creator={agent.creator}
|
||||
shortDescription={agent.sub_heading}
|
||||
longDescription={agent.description}
|
||||
rating={agent.rating}
|
||||
runs={agent.runs}
|
||||
categories={agent.categories}
|
||||
lastUpdated={agent.updated_at}
|
||||
version={agent.versions[agent.versions.length - 1]}
|
||||
storeListingVersionId={agent.store_listing_version_id}
|
||||
libraryAgent={libraryAgent}
|
||||
/>
|
||||
</div>
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
<AgentsSection
|
||||
margin="32px"
|
||||
agents={otherAgents.agents}
|
||||
sectionTitle={`Other agents by ${agent.creator}`}
|
||||
<AgentImages
|
||||
images={
|
||||
agent.agent_video
|
||||
? [agent.agent_video, ...agent.agent_image]
|
||||
: agent.agent_image
|
||||
}
|
||||
/>
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
<AgentsSection
|
||||
margin="32px"
|
||||
agents={similarAgents.agents}
|
||||
sectionTitle="Similar agents"
|
||||
/>
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
<BecomeACreator
|
||||
title="Become a Creator"
|
||||
description="Join our ever-growing community of hackers and tinkerers"
|
||||
buttonText="Become a Creator"
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="mb-9 mt-7" />
|
||||
<AgentsSection
|
||||
agents={otherAgents.agents}
|
||||
sectionTitle={`Other agents by ${agent.creator}`}
|
||||
/>
|
||||
<Separator className="mb-9 mt-11" />
|
||||
<AgentsSection
|
||||
agents={similarAgents.agents}
|
||||
sectionTitle="Similar agents"
|
||||
/>
|
||||
<Separator className="mb-9 mt-11" />
|
||||
<BecomeACreator title="Become a Creator" buttonText="Become a Creator" />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,50 +44,47 @@ export default async function Page({
|
||||
const creatorAgents = await api.getStoreAgents({ creator: params.creator });
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-screen max-w-[1360px]">
|
||||
<main className="mt-5 px-4">
|
||||
<BreadCrumbs
|
||||
items={[
|
||||
{ name: "Store", link: "/marketplace" },
|
||||
{ name: creator.name, link: "#" },
|
||||
]}
|
||||
/>
|
||||
<main className="mt-9 px-10">
|
||||
<BreadCrumbs
|
||||
items={[
|
||||
{ name: "Store", link: "/marketplace" },
|
||||
{ name: creator.name, link: "#" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="mt-4 flex flex-col items-start gap-4 sm:mt-6 sm:gap-6 md:mt-8 md:flex-row md:gap-8">
|
||||
<div className="w-full md:w-auto md:shrink-0">
|
||||
<CreatorInfoCard
|
||||
username={creator.name}
|
||||
handle={creator.username}
|
||||
avatarSrc={creator.avatar_url}
|
||||
categories={creator.top_categories}
|
||||
averageRating={creator.agent_rating}
|
||||
totalRuns={creator.agent_runs}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-4 sm:gap-6 md:gap-8">
|
||||
<p className="font-geist text-underline-position-from-font text-decoration-skip-none text-left text-base font-medium leading-6">
|
||||
About
|
||||
</p>
|
||||
<div
|
||||
className="font-poppins text-[48px] font-normal leading-[59px] text-neutral-900 dark:text-zinc-50"
|
||||
style={{ whiteSpace: "pre-line" }}
|
||||
>
|
||||
{creator.description}
|
||||
</div>
|
||||
|
||||
<CreatorLinks links={creator.links} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 sm:mt-12 md:mt-16 lg:pb-[58px]">
|
||||
<Separator className="mb-6 bg-gray-200" />
|
||||
<AgentsSection
|
||||
agents={creatorAgents.agents}
|
||||
hideAvatars={true}
|
||||
sectionTitle={`Agents by ${creator.name}`}
|
||||
<div className="mt-4 flex flex-col items-start gap-4 sm:mt-6 sm:gap-6 md:mt-[3.2rem] md:flex-row md:gap-10">
|
||||
<div>
|
||||
<CreatorInfoCard
|
||||
username={creator.name}
|
||||
handle={creator.username}
|
||||
avatarSrc={creator.avatar_url}
|
||||
categories={creator.top_categories}
|
||||
averageRating={creator.agent_rating}
|
||||
totalRuns={creator.agent_runs}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<div className="flex-1 space-y-7">
|
||||
<div className="space-y-3">
|
||||
<p className="font-sans text-base font-medium text-zinc-800">
|
||||
About
|
||||
</p>
|
||||
<h1 className="font-poppins text-4xl font-normal leading-[3.25rem] text-zinc-800">
|
||||
{creator.description}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<CreatorLinks links={creator.links} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="mb-9 mt-12 bg-gray-200" />
|
||||
<AgentsSection
|
||||
className="mb-36"
|
||||
agents={creatorAgents.agents}
|
||||
hideAvatars={true}
|
||||
sectionTitle={`Agents by ${creator.name}`}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
} catch (error) {
|
||||
return (
|
||||
|
||||
@@ -149,27 +149,28 @@ export default async function Page({}: {}) {
|
||||
const { featuredAgents, topAgents, featuredCreators } = await getStoreData();
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-screen max-w-[1360px]">
|
||||
<main className="px-4">
|
||||
<main>
|
||||
<section className="px-4 md:px-10">
|
||||
<HeroSection />
|
||||
<FeaturedSection featuredAgents={featuredAgents.agents} />
|
||||
{/* 100px margin because our featured sections button are placed 40px below the container */}
|
||||
<Separator className="mb-6 mt-24" />
|
||||
</section>
|
||||
<FeaturedSection featuredAgents={featuredAgents.agents} />
|
||||
<section className="px-4 md:px-10">
|
||||
{/* Below Separator's mt is 44px as per design; I need to add extra to counter the absolute positioning of the arrows above */}
|
||||
<Separator className="mb-9 mt-18" />
|
||||
<AgentsSection
|
||||
sectionTitle="Top Agents"
|
||||
agents={topAgents.agents as Agent[]}
|
||||
/>
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
<Separator className="mb-9 mt-11" />
|
||||
<FeaturedCreators
|
||||
featuredCreators={featuredCreators.creators as FeaturedCreator[]}
|
||||
/>
|
||||
<Separator className="mb-[25px] mt-[60px]" />
|
||||
<Separator className="mb-9 mt-11" />
|
||||
<BecomeACreator
|
||||
title="Become a Creator"
|
||||
description="Join our ever-growing community of hackers and tinkerers"
|
||||
buttonText="Become a Creator"
|
||||
buttonText="Upload your agent"
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,67 +116,67 @@ function SearchResults({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mx-auto min-h-screen max-w-[1440px] px-10 lg:min-w-[1440px]">
|
||||
<div className="mt-8 flex items-center">
|
||||
<div className="flex-1">
|
||||
<h2 className="font-geist text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
Results for:
|
||||
</h2>
|
||||
<h1 className="font-poppins text-2xl font-semibold leading-[32px] text-neutral-800 dark:text-neutral-100">
|
||||
{searchTerm}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<SearchBar width="w-[439px]" height="h-[60px]" />
|
||||
</div>
|
||||
<div className="px-10">
|
||||
<div className="mt-9 flex items-center">
|
||||
<div className="flex-1">
|
||||
<h2 className="font-sans text-base font-medium leading-normal text-zinc-800 dark:text-neutral-200">
|
||||
Results for:
|
||||
</h2>
|
||||
<h1 className="font-poppins text-2xl font-medium leading-[32px] text-zinc-800 dark:text-neutral-100">
|
||||
{searchTerm}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<SearchBar className="w-[28rem]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="mt-20 flex flex-col items-center justify-center">
|
||||
<p className="text-neutral-500 dark:text-neutral-400">Loading...</p>
|
||||
{isLoading ? (
|
||||
<div className="mt-20 flex flex-col items-center justify-center">
|
||||
<p className="text-neutral-500 dark:text-neutral-400">Loading...</p>
|
||||
</div>
|
||||
) : totalCount > 0 ? (
|
||||
<>
|
||||
<div className="mt-9 flex items-center justify-between">
|
||||
<SearchFilterChips
|
||||
totalCount={totalCount}
|
||||
agentsCount={agentsCount}
|
||||
creatorsCount={creatorsCount}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
<SortDropdown onSort={handleSortChange} />
|
||||
</div>
|
||||
) : totalCount > 0 ? (
|
||||
<>
|
||||
<div className="mt-[36px] flex items-center justify-between">
|
||||
<SearchFilterChips
|
||||
totalCount={totalCount}
|
||||
agentsCount={agentsCount}
|
||||
creatorsCount={creatorsCount}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
<SortDropdown onSort={handleSortChange} />
|
||||
</div>
|
||||
{/* Content section */}
|
||||
<div className="min-h-[500px] max-w-[1440px]">
|
||||
{showAgents && agentsCount > 0 && (
|
||||
<div className="mt-[36px]">
|
||||
<AgentsSection agents={agents} sectionTitle="Agents" />
|
||||
</div>
|
||||
)}
|
||||
{/* Content section */}
|
||||
<div className="space-y-9 py-9">
|
||||
{showAgents && agentsCount > 0 && (
|
||||
<div>
|
||||
<AgentsSection agents={agents} sectionTitle="Agents" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAgents && agentsCount > 0 && creatorsCount > 0 && (
|
||||
<Separator />
|
||||
)}
|
||||
{showCreators && creatorsCount > 0 && (
|
||||
{showAgents && agentsCount > 0 && creatorsCount > 0 && (
|
||||
<Separator />
|
||||
)}
|
||||
{showCreators && creatorsCount > 0 && (
|
||||
<div>
|
||||
<FeaturedCreators
|
||||
featuredCreators={creators}
|
||||
title="Creators"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-20 flex flex-col items-center justify-center">
|
||||
<h3 className="mb-2 text-xl font-medium text-neutral-600 dark:text-neutral-300">
|
||||
No results found
|
||||
</h3>
|
||||
<p className="text-neutral-500 dark:text-neutral-400">
|
||||
Try adjusting your search terms or filters
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-20 flex flex-col items-center justify-center">
|
||||
<h3 className="mb-2 text-xl font-medium text-neutral-600 dark:text-neutral-300">
|
||||
No results found
|
||||
</h3>
|
||||
<p className="text-neutral-500 dark:text-neutral-400">
|
||||
Try adjusting your search terms or filters
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@ import { APIKeysSection } from "@/components/agptui/composite/APIKeySection";
|
||||
|
||||
const ApiKeysPage = () => {
|
||||
return (
|
||||
<div className="w-full pr-4 pt-24 md:pt-0">
|
||||
<main className="flex-1 space-y-7.5 pb-8">
|
||||
<h1 className="font-poppins text-[1.75rem] font-medium leading-[2.5rem] text-zinc-500">
|
||||
API key
|
||||
</h1>
|
||||
<APIKeysSection />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -104,8 +104,8 @@ export default function CreditsPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full px-4 sm:px-8 md:min-w-[800px]">
|
||||
<h1 className="mb-6 text-[28px] font-normal text-neutral-900 dark:text-neutral-100 sm:mb-8 sm:text-[35px]">
|
||||
<div className="flex-1 space-y-7.5 pb-8">
|
||||
<h1 className="font-poppins text-[1.75rem] font-medium leading-[2.5rem] text-zinc-500">
|
||||
Billing
|
||||
</h1>
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
import useSupabase from "@/hooks/useSupabase";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import AutogptButton from "@/components/agptui/AutogptButton";
|
||||
|
||||
export default function Page({}: {}) {
|
||||
const { supabase } = useSupabase();
|
||||
@@ -64,70 +65,71 @@ export default function Page({}: {}) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<main className="flex-1 py-8">
|
||||
{/* Header Section */}
|
||||
<div className="mb-8 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-4xl font-medium text-neutral-900 dark:text-neutral-100">
|
||||
Agent dashboard
|
||||
</h1>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-xl font-medium text-neutral-900 dark:text-neutral-100">
|
||||
<main className="flex-1 space-y-7.5 pb-8">
|
||||
{/* Title */}
|
||||
<h1 className="font-poppins text-[1.75rem] font-medium leading-[2.5rem] text-zinc-500">
|
||||
Agent dashboard
|
||||
</h1>
|
||||
|
||||
{/* Content */}
|
||||
<section className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="font-poppins text-base font-medium text-zinc-800">
|
||||
Submit a New Agent
|
||||
</h2>
|
||||
<p className="text-sm text-[#707070] dark:text-neutral-400">
|
||||
|
||||
<p className="font-sans text-base font-normal text-zinc-600">
|
||||
Select from the list of agents you currently have, or upload from
|
||||
your local machine.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<PublishAgentPopout
|
||||
trigger={
|
||||
<Button
|
||||
onClick={onOpenPopout}
|
||||
className="h-9 rounded-full bg-black px-4 text-sm font-medium text-white hover:bg-neutral-700 dark:hover:bg-neutral-600"
|
||||
>
|
||||
Submit agent
|
||||
</Button>
|
||||
}
|
||||
openPopout={openPopout}
|
||||
inputStep={popoutStep}
|
||||
submissionData={submissionData}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator className="mb-8" />
|
||||
|
||||
{/* Agents Section */}
|
||||
<div>
|
||||
<h2 className="mb-4 text-xl font-bold text-neutral-900 dark:text-neutral-100">
|
||||
Your uploaded agents
|
||||
</h2>
|
||||
{submissions && (
|
||||
<AgentTable
|
||||
agents={
|
||||
submissions?.submissions.map((submission, index) => ({
|
||||
id: index,
|
||||
agent_id: submission.agent_id,
|
||||
agent_version: submission.agent_version,
|
||||
sub_heading: submission.sub_heading,
|
||||
date_submitted: submission.date_submitted,
|
||||
agentName: submission.name,
|
||||
description: submission.description,
|
||||
imageSrc: submission.image_urls || [""],
|
||||
dateSubmitted: new Date(
|
||||
submission.date_submitted,
|
||||
).toLocaleDateString(),
|
||||
status: submission.status.toLowerCase() as StatusType,
|
||||
runs: submission.runs,
|
||||
rating: submission.rating,
|
||||
})) || []
|
||||
<PublishAgentPopout
|
||||
trigger={
|
||||
<AutogptButton onClick={onOpenPopout} variant="outline">
|
||||
Add to Library
|
||||
</AutogptButton>
|
||||
}
|
||||
onEditSubmission={onEditSubmission}
|
||||
onDeleteSubmission={onDeleteSubmission}
|
||||
openPopout={openPopout}
|
||||
inputStep={popoutStep}
|
||||
submissionData={submissionData}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="bg-neutral-300" />
|
||||
|
||||
<div className="space-y-3">
|
||||
<h2 className="font-poppins text-base font-medium text-zinc-800">
|
||||
Your uploaded agents
|
||||
</h2>
|
||||
|
||||
{submissions && (
|
||||
<AgentTable
|
||||
agents={
|
||||
submissions?.submissions.map((submission, index) => ({
|
||||
id: index,
|
||||
agent_id: submission.agent_id,
|
||||
agent_version: submission.agent_version,
|
||||
sub_heading: submission.sub_heading,
|
||||
date_submitted: submission.date_submitted,
|
||||
agentName: submission.name,
|
||||
description: submission.description,
|
||||
imageSrc: submission.image_urls || [""],
|
||||
dateSubmitted: new Date(
|
||||
submission.date_submitted,
|
||||
).toLocaleDateString(),
|
||||
status: submission.status.toLowerCase() as StatusType,
|
||||
runs: submission.runs,
|
||||
rating: submission.rating,
|
||||
})) || []
|
||||
}
|
||||
onEditSubmission={onEditSubmission}
|
||||
onDeleteSubmission={onDeleteSubmission}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -149,83 +149,91 @@ export default function PrivatePage() {
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl md:py-8">
|
||||
<h2 className="mb-4 text-lg">Connections & Credentials</h2>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Provider</TableHead>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{allCredentials.map((cred) => (
|
||||
<TableRow key={cred.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<cred.ProviderIcon className="h-4 w-4" />
|
||||
<strong>{cred.providerName}</strong>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex h-full items-center space-x-1.5">
|
||||
<cred.TypeIcon />
|
||||
<span>{cred.title || cred.username}</span>
|
||||
</div>
|
||||
<small className="text-muted-foreground">
|
||||
{
|
||||
{
|
||||
oauth2: "OAuth2 credentials",
|
||||
api_key: "API key",
|
||||
user_password: "Username & password",
|
||||
}[cred.type]
|
||||
}{" "}
|
||||
- <code>{cred.id}</code>
|
||||
</small>
|
||||
</TableCell>
|
||||
<TableCell className="w-0 whitespace-nowrap">
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => removeCredentials(cred.provider, cred.id)}
|
||||
>
|
||||
<Trash2Icon className="mr-1.5 size-4" /> Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
<div className="space-y-6 pb-10">
|
||||
<h1 className="font-poppins text-[1.75rem] font-medium leading-[2.5rem] text-zinc-500">
|
||||
Profile
|
||||
</h1>
|
||||
<div>
|
||||
<h2 className="font-poppins text-base font-medium text-zinc-800">
|
||||
Connections & Credentials
|
||||
</h2>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Provider</TableHead>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{allCredentials.map((cred) => (
|
||||
<TableRow key={cred.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<cred.ProviderIcon className="h-4 w-4" />
|
||||
<strong>{cred.providerName}</strong>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex h-full items-center space-x-1.5">
|
||||
<cred.TypeIcon />
|
||||
<span>{cred.title || cred.username}</span>
|
||||
</div>
|
||||
<small className="text-muted-foreground">
|
||||
{
|
||||
{
|
||||
oauth2: "OAuth2 credentials",
|
||||
api_key: "API key",
|
||||
user_password: "Username & password",
|
||||
}[cred.type]
|
||||
}{" "}
|
||||
- <code>{cred.id}</code>
|
||||
</small>
|
||||
</TableCell>
|
||||
<TableCell className="w-0 whitespace-nowrap">
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => removeCredentials(cred.provider, cred.id)}
|
||||
>
|
||||
<Trash2Icon className="mr-1.5 size-4" /> Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<AlertDialog open={confirmationDialogState.open}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{confirmationDialogState.open && confirmationDialogState.message}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel
|
||||
onClick={() =>
|
||||
confirmationDialogState.open &&
|
||||
confirmationDialogState.onReject()
|
||||
}
|
||||
>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
variant="destructive"
|
||||
onClick={() =>
|
||||
confirmationDialogState.open &&
|
||||
confirmationDialogState.onConfirm()
|
||||
}
|
||||
>
|
||||
Continue
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<AlertDialog open={confirmationDialogState.open}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{confirmationDialogState.open &&
|
||||
confirmationDialogState.message}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel
|
||||
onClick={() =>
|
||||
confirmationDialogState.open &&
|
||||
confirmationDialogState.onReject()
|
||||
}
|
||||
>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
variant="destructive"
|
||||
onClick={() =>
|
||||
confirmationDialogState.open &&
|
||||
confirmationDialogState.onConfirm()
|
||||
}
|
||||
>
|
||||
Continue
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,47 +14,49 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
{
|
||||
links: [
|
||||
{
|
||||
text: "Creator Dashboard",
|
||||
href: "/profile/dashboard",
|
||||
icon: <IconDashboardLayout className="h-6 w-6" />,
|
||||
text: "API Keys",
|
||||
href: "/profile/api_keys",
|
||||
icon: <KeyIcon className="h-6 w-6 stroke-[1.25px]" />,
|
||||
},
|
||||
...(process.env.NEXT_PUBLIC_SHOW_BILLING_PAGE === "true"
|
||||
? [
|
||||
{
|
||||
text: "Billing",
|
||||
href: "/profile/credits",
|
||||
icon: <IconCoin className="h-6 w-6" />,
|
||||
icon: <IconCoin className="h-6 w-6 stroke-[1.25px]" />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
text: "Creator Dashboard",
|
||||
href: "/profile/dashboard",
|
||||
icon: <IconDashboardLayout className="h-6 w-6 stroke-[1.25px]" />,
|
||||
},
|
||||
|
||||
{
|
||||
text: "Integrations",
|
||||
href: "/profile/integrations",
|
||||
icon: <IconIntegrations className="h-6 w-6" />,
|
||||
},
|
||||
{
|
||||
text: "API Keys",
|
||||
href: "/profile/api_keys",
|
||||
icon: <KeyIcon className="h-6 w-6" />,
|
||||
icon: <IconIntegrations className="h-6 w-6 stroke-[1.25px]" />,
|
||||
},
|
||||
|
||||
{
|
||||
text: "Profile",
|
||||
href: "/profile",
|
||||
icon: <IconProfile className="h-6 w-6" />,
|
||||
icon: <IconProfile className="h-6 w-6 stroke-[1.25px]" />,
|
||||
},
|
||||
{
|
||||
text: "Settings",
|
||||
href: "/profile/settings",
|
||||
icon: <IconSliders className="h-6 w-6" />,
|
||||
icon: <IconSliders className="h-6 w-6 stroke-[1.25px]" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen w-screen max-w-[1360px] flex-col lg:flex-row">
|
||||
<div className="flex flex-row gap-14 px-4 pr-10 pt-8">
|
||||
<Sidebar linkGroups={sidebarLinkGroups} />
|
||||
<div className="flex-1 pl-4">{children}</div>
|
||||
<div className="flex-1">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,10 @@ export default async function Page({}: {}) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center px-4">
|
||||
<div className="space-y-6 pb-10">
|
||||
<h1 className="font-poppins text-[1.75rem] font-medium leading-[2.5rem] text-zinc-500">
|
||||
Profile
|
||||
</h1>
|
||||
<ProfileInfoForm profile={profile as CreatorDetails} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -19,13 +19,12 @@ export default async function SettingsPage() {
|
||||
const preferences = await getUserPreferences();
|
||||
|
||||
return (
|
||||
<div className="container max-w-2xl space-y-6 py-10">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">My account</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Manage your account settings and preferences.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-6 pb-10">
|
||||
{/* Title */}
|
||||
<h1 className="font-poppins text-[1.75rem] font-medium leading-[2.5rem] text-zinc-500">
|
||||
Settings
|
||||
</h1>
|
||||
|
||||
<SettingsForm user={user} preferences={preferences} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -38,7 +38,7 @@ export default async function RootLayout({
|
||||
>
|
||||
<body
|
||||
className={cn(
|
||||
"bg-neutral-50 antialiased transition-colors",
|
||||
"bg-white antialiased transition-colors",
|
||||
inter.className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -8,15 +8,30 @@ import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import CredentialsProvider from "@/components/integrations/credentials-provider";
|
||||
import { LaunchDarklyProvider } from "@/components/feature-flag/feature-flag-provider";
|
||||
import OnboardingProvider from "@/components/onboarding/onboarding-provider";
|
||||
import { MockClientProps } from "@/lib/autogpt-server-api/mock_client";
|
||||
import PageStructureContainer from "@/components/page-structure-container-provider";
|
||||
|
||||
export function Providers({ children, ...props }: ThemeProviderProps) {
|
||||
export interface ProvidersProps extends ThemeProviderProps {
|
||||
children: React.ReactNode;
|
||||
useMockBackend?: boolean;
|
||||
mockClientProps?: MockClientProps;
|
||||
}
|
||||
|
||||
export function Providers({
|
||||
children,
|
||||
useMockBackend,
|
||||
mockClientProps,
|
||||
...props
|
||||
}: ProvidersProps) {
|
||||
return (
|
||||
<NextThemesProvider {...props}>
|
||||
<BackendAPIProvider>
|
||||
<BackendAPIProvider mockClientProps={mockClientProps}>
|
||||
<CredentialsProvider>
|
||||
<LaunchDarklyProvider>
|
||||
<OnboardingProvider>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<PageStructureContainer>{children}</PageStructureContainer>
|
||||
</TooltipProvider>
|
||||
</OnboardingProvider>
|
||||
</LaunchDarklyProvider>
|
||||
</CredentialsProvider>
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from "react";
|
||||
import Image from "next/image";
|
||||
import { PlayIcon } from "@radix-ui/react-icons";
|
||||
import { Button } from "./Button";
|
||||
import AutogptButton from "./AutogptButton";
|
||||
|
||||
const isValidVideoFile = (url: string): boolean => {
|
||||
const videoExtensions = /\.(mp4|webm|ogg)$/i;
|
||||
@@ -32,6 +33,8 @@ interface AgentImageItemProps {
|
||||
export const AgentImageItem: React.FC<AgentImageItemProps> = React.memo(
|
||||
({ image, index, playingVideoIndex, handlePlay, handlePause }) => {
|
||||
const videoRef = React.useRef<HTMLVideoElement>(null);
|
||||
const [isVideoPlaying, setIsVideoPlaying] = React.useState(false);
|
||||
const [thumbnail, setThumbnail] = React.useState<string>("");
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
@@ -43,11 +46,22 @@ export const AgentImageItem: React.FC<AgentImageItemProps> = React.memo(
|
||||
}
|
||||
}, [playingVideoIndex, index]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (videoRef.current && isValidVideoFile(image)) {
|
||||
videoRef.current.currentTime = 0.1;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = videoRef.current.videoWidth;
|
||||
canvas.height = videoRef.current.videoHeight;
|
||||
canvas.getContext("2d")?.drawImage(videoRef.current, 0, 0);
|
||||
setThumbnail(canvas.toDataURL());
|
||||
}
|
||||
}, [image]);
|
||||
|
||||
const isVideoFile = isValidVideoFile(image);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="h-[15rem] overflow-hidden rounded-[26px] bg-[#a8a8a8] dark:bg-neutral-700 sm:h-[20rem] sm:w-full md:h-[25rem] lg:h-[30rem]">
|
||||
<div className="h-[15rem] overflow-hidden rounded-[1.5rem] bg-[#a8a8a8] dark:bg-neutral-700 sm:h-[20rem] sm:w-full md:h-[25rem] lg:h-[32rem]">
|
||||
{isValidVideoUrl(image) ? (
|
||||
getYouTubeVideoId(image) ? (
|
||||
<iframe
|
||||
@@ -63,12 +77,18 @@ export const AgentImageItem: React.FC<AgentImageItemProps> = React.memo(
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
controls
|
||||
controls={isVideoPlaying}
|
||||
preload="metadata"
|
||||
poster={`${image}#t=0.1`}
|
||||
poster={thumbnail || `${image}#t=0.1`}
|
||||
style={{ objectPosition: "center 25%" }}
|
||||
onPlay={() => handlePlay(index)}
|
||||
onPause={() => handlePause(index)}
|
||||
onPlay={() => {
|
||||
setIsVideoPlaying(true);
|
||||
handlePlay(index);
|
||||
}}
|
||||
onPause={() => {
|
||||
setIsVideoPlaying(false);
|
||||
handlePause(index);
|
||||
}}
|
||||
autoPlay={false}
|
||||
title="Video"
|
||||
>
|
||||
@@ -83,27 +103,24 @@ export const AgentImageItem: React.FC<AgentImageItemProps> = React.memo(
|
||||
src={image}
|
||||
alt="Image"
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
className="rounded-xl object-cover"
|
||||
className="rounded-[1.5rem] object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isVideoFile && playingVideoIndex !== index && (
|
||||
<div className="absolute bottom-2 left-2 sm:bottom-3 sm:left-3 md:bottom-4 md:left-4 lg:bottom-[1.25rem] lg:left-[1.25rem]">
|
||||
<Button
|
||||
size="default"
|
||||
<div className="absolute bottom-2 left-2 sm:bottom-3 sm:left-3 md:bottom-4 md:left-4 lg:bottom-6 lg:left-6">
|
||||
<AutogptButton
|
||||
icon
|
||||
variant={"secondary"}
|
||||
onClick={() => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.play();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="pr-1 font-neue text-sm font-medium leading-6 tracking-tight text-[#272727] dark:text-neutral-200 sm:pr-2 sm:text-base sm:leading-7 md:text-lg md:leading-8 lg:text-xl lg:leading-9">
|
||||
Play demo
|
||||
</span>
|
||||
<PlayIcon className="h-5 w-5 text-black dark:text-neutral-200 sm:h-6 sm:w-6 md:h-7 md:w-7" />
|
||||
</Button>
|
||||
Play
|
||||
</AutogptButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,15 +2,15 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { AgentImages } from "./AgentImages";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Agent Images",
|
||||
title: "Agpt UI/marketing/Agent Images",
|
||||
component: AgentImages,
|
||||
parameters: {
|
||||
layout: {
|
||||
center: true,
|
||||
fullscreen: true,
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="mx-auto flex h-full w-[80%] items-center justify-center p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
images: { control: "object" },
|
||||
@@ -25,34 +25,7 @@ export const Default: Story = {
|
||||
images: [
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
"https://youtu.be/KWonAsyKF3g?si=JMibxlN_6OVo6LhJ",
|
||||
"https://storage.googleapis.com/agpt-dev-website-media/DJINeo.mp4",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const OnlyImages: Story = {
|
||||
args: {
|
||||
images: [
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
"https://upload.wikimedia.org/wikipedia/commons/c/c5/Big_buck_bunny_poster_big.jpg",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithVideos: Story = {
|
||||
args: {
|
||||
images: [
|
||||
"https://storage.googleapis.com/agpt-dev-website-media/DJINeo.mp4",
|
||||
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
|
||||
"https://youtu.be/KWonAsyKF3g?si=JMibxlN_6OVo6LhJ",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const SingleItem: Story = {
|
||||
args: {
|
||||
images: [
|
||||
"https://upload.wikimedia.org/wikipedia/commons/c/c5/Big_buck_bunny_poster_big.jpg",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -26,19 +26,17 @@ export const AgentImages: React.FC<AgentImagesProps> = ({ images }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-y-auto bg-white px-2 dark:bg-transparent lg:w-[56.25rem]">
|
||||
<div className="space-y-4 sm:space-y-6 md:space-y-[1.875rem]">
|
||||
{images.map((image, index) => (
|
||||
<AgentImageItem
|
||||
key={index}
|
||||
image={image}
|
||||
index={index}
|
||||
playingVideoIndex={playingVideoIndex}
|
||||
handlePlay={handlePlay}
|
||||
handlePause={handlePause}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="w-full space-y-4">
|
||||
{images.map((image, index) => (
|
||||
<AgentImageItem
|
||||
key={index}
|
||||
image={image}
|
||||
index={index}
|
||||
playingVideoIndex={playingVideoIndex}
|
||||
handlePlay={handlePlay}
|
||||
handlePause={handlePause}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { AgentInfo } from "./AgentInfo";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Agent Info",
|
||||
title: "Agpt UI/marketing/Agent Info",
|
||||
component: AgentInfo,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
layout: {
|
||||
center: true,
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
@@ -19,7 +21,15 @@ const meta = {
|
||||
categories: { control: "object" },
|
||||
lastUpdated: { control: "text" },
|
||||
version: { control: "text" },
|
||||
storeListingVersionId: { control: "text" },
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof AgentInfo>;
|
||||
|
||||
export default meta;
|
||||
@@ -30,10 +40,10 @@ export const Default: Story = {
|
||||
user: null,
|
||||
libraryAgent: null,
|
||||
name: "AI Video Generator",
|
||||
storeListingVersionId: "123",
|
||||
storeListingVersionId: "123abc456def",
|
||||
creator: "Toran Richards",
|
||||
shortDescription:
|
||||
"Transform ideas into breathtaking images with this AI-powered Image Generator.",
|
||||
"Transform ideas into breathtaking videos with this AI-powered Video Generator.",
|
||||
longDescription: `Create Viral-Ready Content in Seconds! Transform trending topics into engaging videos with this cutting-edge AI Video Generator. Perfect for content creators, social media managers, and marketers looking to quickly produce high-quality content.
|
||||
|
||||
Key features include:
|
||||
@@ -51,87 +61,52 @@ Key features include:
|
||||
},
|
||||
};
|
||||
|
||||
export const LowRating: Story = {
|
||||
export const LongContent: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
name: "Data Analyzer",
|
||||
creator: "DataTech",
|
||||
shortDescription:
|
||||
"Analyze complex datasets with machine learning algorithms",
|
||||
longDescription:
|
||||
"A comprehensive data analysis tool that leverages machine learning to provide deep insights into your datasets. Currently in beta testing phase.",
|
||||
rating: 2.7,
|
||||
runs: 5000,
|
||||
categories: ["Data Analysis", "Machine Learning"],
|
||||
lastUpdated: "1 week ago",
|
||||
version: "0.9.5",
|
||||
name: "Super Advanced Ultra-Intelligent Universal Comprehensive AI-Powered Video Generator Pro Plus Premium Enterprise Edition With Extended Capabilities",
|
||||
creator:
|
||||
"Global Artificial Intelligence Research and Development Consortium for Advanced Technology Implementation and Enterprise Solutions",
|
||||
longDescription: `Create Viral-Ready Content in Seconds! Transform trending topics into engaging videos with this cutting-edge AI Video Generator. Perfect for content creators, social media managers, and marketers looking to quickly produce high-quality content.
|
||||
|
||||
Our advanced AI algorithms analyze current trends and viewer preferences to generate videos that are more likely to engage your target audience and achieve better conversion rates. The system adapts to your brand voice and style guidelines to maintain consistency across all your content.
|
||||
|
||||
Key features include:
|
||||
- Customizable video output with adjustable parameters for length, style, pacing, and transition effects
|
||||
- 15+ pre-made templates for different content types including explainer videos, product demonstrations, social media stories, and advertisements
|
||||
- Auto scene detection that intelligently segments your content into engaging chapters
|
||||
- Smart text-to-speech with over 50 natural-sounding voices in multiple languages and dialects
|
||||
- Multiple export formats optimized for different platforms (YouTube, Instagram, TikTok, Facebook, Twitter)
|
||||
- SEO-optimized suggestions for titles, descriptions, and tags to maximize discoverability
|
||||
- Analytics dashboard to track performance metrics and audience engagement
|
||||
- Collaborative workspace for team projects with permission management
|
||||
- Regular updates with new features and improvements based on user feedback
|
||||
|
||||
The AI Video Generator integrates seamlessly with your existing workflow and content management systems. You can import assets from Adobe Creative Suite, Canva, and other popular design tools. Our cloud-based processing ensures fast rendering without taxing your local system resources.
|
||||
|
||||
With our enterprise plan, you'll get priority support, custom template development, and advanced branding options to ensure your videos stand out in today's crowded digital landscape.`,
|
||||
|
||||
categories: [
|
||||
"Video",
|
||||
"Content Creation",
|
||||
"Social Media",
|
||||
"Marketing",
|
||||
"Artificial Intelligence",
|
||||
"Machine Learning",
|
||||
"Neural Networks",
|
||||
"Deep Learning",
|
||||
"Computer Vision",
|
||||
"NLP",
|
||||
"Automation",
|
||||
"Productivity Tools",
|
||||
],
|
||||
runs: 1000000000,
|
||||
},
|
||||
};
|
||||
|
||||
export const HighRuns: Story = {
|
||||
export const NoCategories: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
name: "Code Assistant",
|
||||
creator: "DevAI",
|
||||
shortDescription:
|
||||
"Get AI-powered coding help for various programming languages",
|
||||
longDescription:
|
||||
"An advanced AI coding assistant that supports multiple programming languages and frameworks. Features include code completion, refactoring suggestions, and bug detection.",
|
||||
rating: 4.8,
|
||||
runs: 1000000,
|
||||
categories: ["Programming", "AI", "Developer Tools"],
|
||||
lastUpdated: "1 day ago",
|
||||
version: "2.1.3",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInteraction: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
name: "Task Planner",
|
||||
creator: "Productivity AI",
|
||||
shortDescription: "Plan and organize your tasks efficiently with AI",
|
||||
longDescription:
|
||||
"An intelligent task management system that helps you organize, prioritize, and complete your tasks more efficiently. Features smart scheduling and AI-powered suggestions.",
|
||||
rating: 4.2,
|
||||
runs: 50000,
|
||||
categories: ["Productivity", "Task Management", "AI"],
|
||||
lastUpdated: "3 days ago",
|
||||
version: "1.5.2",
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Test run agent button
|
||||
const runButton = canvas.getByText("Run agent");
|
||||
await userEvent.hover(runButton);
|
||||
await userEvent.click(runButton);
|
||||
|
||||
// Test rating interaction
|
||||
const ratingStars = canvas.getAllByLabelText(/Star Icon/);
|
||||
await userEvent.hover(ratingStars[3]);
|
||||
await userEvent.click(ratingStars[3]);
|
||||
|
||||
// Test category interaction
|
||||
const category = canvas.getByText("Productivity");
|
||||
await userEvent.hover(category);
|
||||
await userEvent.click(category);
|
||||
},
|
||||
};
|
||||
|
||||
export const LongDescription: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
name: "AI Writing Assistant",
|
||||
creator: "WordCraft AI",
|
||||
shortDescription:
|
||||
"Enhance your writing with our advanced AI-powered assistant.",
|
||||
longDescription:
|
||||
"It offers real-time suggestions for grammar, style, and tone, helps with research and fact-checking, and can even generate content ideas based on your input.",
|
||||
rating: 4.7,
|
||||
runs: 75000,
|
||||
categories: ["Writing", "AI", "Content Creation"],
|
||||
lastUpdated: "5 days ago",
|
||||
version: "3.0.1",
|
||||
categories: [],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,6 +8,8 @@ import Link from "next/link";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
|
||||
import { useOnboarding } from "../onboarding/onboarding-provider";
|
||||
import AutogptButton from "./AutogptButton";
|
||||
import { Chip } from "./Chip";
|
||||
import { User } from "@supabase/supabase-js";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { FC, useCallback, useMemo, useState } from "react";
|
||||
@@ -78,7 +80,7 @@ export const AgentInfo: FC<AgentInfoProps> = ({
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}, [toast, api, storeListingVersionId, completeStep, router]);
|
||||
}, [toast, api, storeListingVersionId, completeStep, router, libraryAgent]);
|
||||
|
||||
const handleDownload = useCallback(async () => {
|
||||
const downloadAgent = async (): Promise<void> => {
|
||||
@@ -125,113 +127,104 @@ export const AgentInfo: FC<AgentInfoProps> = ({
|
||||
}, [setDownloading, api, storeListingVersionId, toast]);
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[396px] px-4 sm:px-6 lg:w-[396px] lg:px-0">
|
||||
{/* Title */}
|
||||
<div className="mb-3 w-full font-poppins text-2xl font-medium leading-normal text-neutral-900 dark:text-neutral-100 sm:text-3xl lg:mb-4 lg:text-[35px] lg:leading-10">
|
||||
{name}
|
||||
</div>
|
||||
<div className="w-full max-w-[27rem] space-y-7">
|
||||
{/* Top part */}
|
||||
<div className="space-y-[3.25rem]">
|
||||
{/* Agent name */}
|
||||
<div>
|
||||
<h2 className="font-poppins text-[1.75rem] font-medium leading-[2.5rem] text-zinc-800">
|
||||
{name}
|
||||
</h2>
|
||||
|
||||
{/* Creator */}
|
||||
<div className="mb-3 flex w-full items-center gap-1.5 lg:mb-4">
|
||||
<div className="font-sans text-base font-normal text-neutral-800 dark:text-neutral-200 sm:text-lg lg:text-xl">
|
||||
by
|
||||
</div>
|
||||
<Link
|
||||
href={`/marketplace/creator/${encodeURIComponent(creator)}`}
|
||||
className="font-sans text-base font-medium text-neutral-800 hover:underline dark:text-neutral-200 sm:text-lg lg:text-xl"
|
||||
>
|
||||
{creator}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Short Description */}
|
||||
<div className="mb-4 line-clamp-2 w-full font-sans text-base font-normal leading-normal text-neutral-600 dark:text-neutral-300 sm:text-lg lg:mb-6 lg:text-xl lg:leading-7">
|
||||
{shortDescription}
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="mb-4 flex w-full gap-3 lg:mb-[60px]">
|
||||
{user && (
|
||||
<button
|
||||
className={cn(
|
||||
"inline-flex min-w-24 items-center justify-center rounded-full bg-violet-600 px-4 py-3",
|
||||
"transition-colors duration-200 hover:bg-violet-500 disabled:bg-zinc-400",
|
||||
)}
|
||||
onClick={libraryAction}
|
||||
disabled={adding}
|
||||
>
|
||||
<span className="justify-start font-sans text-sm font-medium leading-snug text-primary-foreground">
|
||||
{libraryAgent ? "See runs" : "Add to library"}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={cn(
|
||||
"inline-flex min-w-24 items-center justify-center rounded-full bg-zinc-200 px-4 py-3",
|
||||
"transition-colors duration-200 hover:bg-zinc-200/70 disabled:bg-zinc-200/40",
|
||||
)}
|
||||
onClick={handleDownload}
|
||||
disabled={downloading}
|
||||
>
|
||||
<div className="justify-start text-center font-sans text-sm font-medium leading-snug text-zinc-800">
|
||||
Download agent
|
||||
{/* Creator name */}
|
||||
<div className="mb-7 flex w-full items-center gap-1.5 font-sans">
|
||||
<p className="text-base font-normal text-zinc-800">by</p>
|
||||
<Link
|
||||
href={`/marketplace/creator/${encodeURIComponent(creator)}`}
|
||||
className="text-base font-medium text-zinc-800 hover:underline"
|
||||
>
|
||||
{creator}
|
||||
</Link>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Rating and Runs */}
|
||||
<div className="mb-4 flex w-full items-center justify-between lg:mb-[44px]">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2">
|
||||
<span className="whitespace-nowrap font-sans text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
|
||||
{rating.toFixed(1)}
|
||||
</span>
|
||||
<div className="flex gap-0.5">{StarRatingIcons(rating)}</div>
|
||||
</div>
|
||||
<div className="whitespace-nowrap font-sans text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
|
||||
{runs.toLocaleString()} runs
|
||||
|
||||
{/* Download and run button */}
|
||||
{/* TODO - Add one more button */}
|
||||
<div className="flex w-full items-center gap-3">
|
||||
{user && (
|
||||
<AutogptButton onClick={libraryAction} disabled={adding} icon>
|
||||
{libraryAgent ? "Run agent" : "Add to library"}
|
||||
</AutogptButton>
|
||||
)}
|
||||
<AutogptButton
|
||||
variant={"secondary"}
|
||||
onClick={handleDownload}
|
||||
disabled={downloading}
|
||||
>
|
||||
Download agent
|
||||
</AutogptButton>
|
||||
</div>
|
||||
|
||||
{/* Runs and ratings */}
|
||||
<div className="flex w-full items-center gap-10">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-sans text-base font-medium text-zinc-800">
|
||||
{rating.toFixed(1)}
|
||||
</span>
|
||||
<div className="flex gap-0.5">{StarRatingIcons(rating)}</div>
|
||||
</div>
|
||||
<div className="font-sans text-base font-medium text-zinc-800">
|
||||
{runs.toLocaleString()} runs
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<Separator className="mb-4 lg:mb-[44px]" />
|
||||
<Separator className="bg-neutral-300" />
|
||||
|
||||
{/* Description Section */}
|
||||
<div className="mb-4 w-full lg:mb-[36px]">
|
||||
<div className="mb-1.5 font-sans text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2">
|
||||
Description
|
||||
{/* Bottom part */}
|
||||
<div className="space-y-9">
|
||||
<div className="space-y-2.5">
|
||||
<p className="font-sans text-base font-medium text-zinc-800">
|
||||
Description
|
||||
</p>
|
||||
<p className="whitespace-pre-line font-sans text-base font-normal text-zinc-600">
|
||||
{longDescription}
|
||||
</p>
|
||||
</div>
|
||||
<div className="whitespace-pre-line font-sans text-base font-normal leading-6 text-neutral-600 dark:text-neutral-400">
|
||||
{longDescription}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="mb-4 flex w-full flex-col gap-1.5 sm:gap-2 lg:mb-[36px]">
|
||||
<div className="decoration-skip-ink-none mb-1.5 font-sans text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2">
|
||||
Categories
|
||||
{/* Categories */}
|
||||
<div className="space-y-2.5">
|
||||
<p className="font-sans text-base font-medium text-zinc-800">
|
||||
Categories
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2.5">
|
||||
{categories.map((category, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
className="hover:border-zinc-400 hover:bg-white"
|
||||
>
|
||||
{category}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5 sm:gap-2">
|
||||
{categories.map((category, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="decoration-skip-ink-none whitespace-nowrap rounded-full border border-neutral-600 bg-white px-2 py-0.5 font-sans text-base font-normal leading-6 text-neutral-800 underline-offset-[from-font] dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-200 sm:px-[16px] sm:py-[10px]"
|
||||
>
|
||||
{category}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Version History */}
|
||||
<div className="flex w-full flex-col gap-0.5 sm:gap-1">
|
||||
<div className="decoration-skip-ink-none mb-1.5 font-sans text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2">
|
||||
Version history
|
||||
</div>
|
||||
<div className="decoration-skip-ink-none font-sans text-base font-normal leading-6 text-neutral-600 underline-offset-[from-font] dark:text-neutral-400">
|
||||
Last updated {lastUpdated}
|
||||
</div>
|
||||
<div className="text-xs text-neutral-600 dark:text-neutral-400 sm:text-sm">
|
||||
Version {version}
|
||||
{/* TODO : Rating Agent */}
|
||||
|
||||
{/* Version History */}
|
||||
<div className="space-y-2.5">
|
||||
<p className="font-sans text-base font-medium text-zinc-800">
|
||||
Version history
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
<p className="font-sans text-base font-normal text-zinc-600">
|
||||
Last updated {lastUpdated}
|
||||
</p>
|
||||
<p className="font-sans text-base font-normal text-zinc-600">
|
||||
Version {version}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { AgentTable } from "./AgentTable";
|
||||
import { AgentTableRowProps } from "./AgentTableRow";
|
||||
import { userEvent, within, expect } from "@storybook/test";
|
||||
import { within, expect, fn } from "@storybook/test";
|
||||
import { StatusType } from "./Status";
|
||||
|
||||
const meta: Meta<typeof AgentTable> = {
|
||||
title: "AGPT UI/Agent Table",
|
||||
const meta = {
|
||||
title: "Agpt UI/marketing/Agent Table",
|
||||
component: AgentTable,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
};
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="container mx-auto p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof AgentTable>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AgentTable>;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const sampleAgents: AgentTableRowProps[] = [
|
||||
const sampleAgents = [
|
||||
{
|
||||
id: 43,
|
||||
agentName: "Super Coder",
|
||||
@@ -22,17 +31,13 @@ const sampleAgents: AgentTableRowProps[] = [
|
||||
"https://ddz4ak4pa3d19.cloudfront.net/cache/53/b2/53b2bc7d7900f0e1e60bf64ebf38032d.jpg",
|
||||
],
|
||||
dateSubmitted: "2023-05-15",
|
||||
status: "approved",
|
||||
status: "approved" as StatusType,
|
||||
runs: 1500,
|
||||
rating: 4.8,
|
||||
agent_id: "43",
|
||||
agent_version: 1,
|
||||
sub_heading: "Super Coder",
|
||||
date_submitted: "2023-05-15",
|
||||
onEditSubmission: () => console.log("Edit Super Coder"),
|
||||
onDeleteSubmission: () => console.log("Delete Super Coder"),
|
||||
selectedAgents: new Set(),
|
||||
setSelectedAgents: () => {},
|
||||
},
|
||||
{
|
||||
id: 44,
|
||||
@@ -42,17 +47,13 @@ const sampleAgents: AgentTableRowProps[] = [
|
||||
"https://ddz4ak4pa3d19.cloudfront.net/cache/40/f7/40f7bc97c952f8df0f9c88d29defe8d4.jpg",
|
||||
],
|
||||
dateSubmitted: "2023-05-10",
|
||||
status: "awaiting_review",
|
||||
status: "awaiting_review" as StatusType,
|
||||
runs: 1200,
|
||||
rating: 4.5,
|
||||
agent_id: "44",
|
||||
agent_version: 1,
|
||||
sub_heading: "Data Analyzer",
|
||||
date_submitted: "2023-05-10",
|
||||
onEditSubmission: () => console.log("Edit Data Analyzer"),
|
||||
onDeleteSubmission: () => console.log("Delete Data Analyzer"),
|
||||
selectedAgents: new Set(),
|
||||
setSelectedAgents: () => {},
|
||||
},
|
||||
{
|
||||
id: 45,
|
||||
@@ -62,48 +63,95 @@ const sampleAgents: AgentTableRowProps[] = [
|
||||
"https://ddz4ak4pa3d19.cloudfront.net/cache/14/9e/149ebb9014aa8c0097e72ed89845af0e.jpg",
|
||||
],
|
||||
dateSubmitted: "2023-05-05",
|
||||
status: "draft",
|
||||
status: "draft" as StatusType,
|
||||
runs: 800,
|
||||
rating: 4.2,
|
||||
agent_id: "45",
|
||||
agent_version: 1,
|
||||
sub_heading: "UI Designer",
|
||||
date_submitted: "2023-05-05",
|
||||
onEditSubmission: () => console.log("Edit UI Designer"),
|
||||
onDeleteSubmission: () => console.log("Delete UI Designer"),
|
||||
selectedAgents: new Set(),
|
||||
setSelectedAgents: () => {},
|
||||
},
|
||||
];
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
agents: sampleAgents,
|
||||
onEditSubmission: fn(() => {
|
||||
console.log("Edit submission");
|
||||
}),
|
||||
onDeleteSubmission: fn(() => {
|
||||
console.log(`Delete submission for agent `);
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyTable: Story = {
|
||||
export const LongContent: Story = {
|
||||
args: {
|
||||
agents: [],
|
||||
...Default.args,
|
||||
agents: [
|
||||
{
|
||||
...sampleAgents[0],
|
||||
agentName:
|
||||
"Super Advanced Artificial Intelligence Code Generator and Optimizer with Machine Learning Capabilities",
|
||||
description:
|
||||
"This is an extremely advanced artificial intelligence code generator that can write clean, efficient, and optimized code in multiple programming languages. It utilizes state-of-the-art machine learning algorithms to understand requirements and generate appropriate solutions while following best practices and design patterns. The agent can handle complex programming tasks, debug existing code, and suggest improvements to enhance performance and readability.",
|
||||
},
|
||||
{
|
||||
...sampleAgents[1],
|
||||
agentName:
|
||||
"Super Advanced Artificial Intelligence Code Generator and Optimizer with Machine Learning Capabilities",
|
||||
description:
|
||||
"A sophisticated data analysis tool capable of processing petabytes of structured and unstructured data to extract meaningful insights and patterns. This agent leverages advanced statistical methods, machine learning techniques, and data visualization capabilities to transform raw data into actionable business intelligence. It can handle time series analysis, predictive modeling, anomaly detection, and generate comprehensive reports with minimal human intervention.",
|
||||
},
|
||||
{
|
||||
...sampleAgents[2],
|
||||
agentName:
|
||||
"Super Advanced Artificial Intelligence Code Generator and Optimizer with Machine Learning Capabilities",
|
||||
description:
|
||||
"This specialized UI/UX design assistant creates beautiful, accessible, and intuitive user interfaces for web and mobile applications. By combining principles of human-centered design with modern aesthetic sensibilities, the agent produces wireframes, mockups, and interactive prototypes that enhance user engagement and satisfaction. It follows design systems, ensures consistent branding, and optimizes layouts for various screen sizes while maintaining accessibility standards.",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Tests
|
||||
export const InteractionTest: Story = {
|
||||
...Default,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const editButtons = await canvas.findAllByText("Edit");
|
||||
await userEvent.click(editButtons[0]);
|
||||
// You would typically assert something here, but console.log is used in the mocked function
|
||||
export const ManyAgents: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
agents: Array(20)
|
||||
.fill(null)
|
||||
.map((_, index) => ({
|
||||
...sampleAgents[index % 3],
|
||||
id: 100 + index,
|
||||
agent_id: `${100 + index}`,
|
||||
agentName: `Test Agent ${index + 1}`,
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyTableTest: Story = {
|
||||
...EmptyTable,
|
||||
args: {
|
||||
...Default.args,
|
||||
agents: [],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const emptyMessage = canvas.getByText("No agents found");
|
||||
expect(emptyMessage).toBeTruthy();
|
||||
const emptyMessages = canvas.getAllByText(
|
||||
"No agents available. Create your first agent to get started!",
|
||||
);
|
||||
await expect(emptyMessages.length).toBeGreaterThan(0);
|
||||
await expect(emptyMessages[0]).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const TestingInteractions: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
agents: sampleAgents,
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const checkboxes = canvas.getAllByTestId("dropdown-button");
|
||||
await expect(checkboxes.length).toBeGreaterThan(0);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,6 +4,15 @@ import * as React from "react";
|
||||
import { AgentTableRow, AgentTableRowProps } from "./AgentTableRow";
|
||||
import { AgentTableCard } from "./AgentTableCard";
|
||||
import { StoreSubmissionRequest } from "@/lib/autogpt-server-api/types";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
|
||||
export interface AgentTableProps {
|
||||
agents: Omit<
|
||||
@@ -40,79 +49,91 @@ export const AgentTable: React.FC<AgentTableProps> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* Table header - Hide on mobile */}
|
||||
<div className="hidden flex-col md:flex">
|
||||
<div className="border-t border-neutral-300 dark:border-neutral-700" />
|
||||
<div className="flex items-center px-4 py-2">
|
||||
<div className="flex items-center">
|
||||
<div className="flex min-w-[120px] items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="selectAllAgents"
|
||||
aria-label="Select all agents"
|
||||
className="mr-4 h-5 w-5 rounded border-2 border-neutral-400 dark:border-neutral-600"
|
||||
checked={
|
||||
selectedAgents.size === agents.length && agents.length > 0
|
||||
}
|
||||
onChange={handleSelectAll}
|
||||
/>
|
||||
<label
|
||||
htmlFor="selectAllAgents"
|
||||
className="text-sm font-medium text-neutral-800 dark:text-neutral-200"
|
||||
>
|
||||
Select all
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-2 grid w-full grid-cols-[400px,150px,150px,100px,100px,50px] items-center">
|
||||
<div className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
|
||||
Agent info
|
||||
</div>
|
||||
<div className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
|
||||
Date submitted
|
||||
</div>
|
||||
<div className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
|
||||
Status
|
||||
</div>
|
||||
<div className="text-right text-sm font-medium text-neutral-800 dark:text-neutral-200">
|
||||
Runs
|
||||
</div>
|
||||
<div className="text-right text-sm font-medium text-neutral-800 dark:text-neutral-200">
|
||||
Reviews
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-b border-neutral-300 dark:border-neutral-700" />
|
||||
<div className="w-full border-t border-neutral-300">
|
||||
{/* Table for desktop view */}
|
||||
<div className="hidden md:block">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>
|
||||
<Checkbox
|
||||
id="selectAllAgents"
|
||||
aria-label="Select all agents"
|
||||
checked={
|
||||
selectedAgents.size === agents.length && agents.length > 0
|
||||
}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedAgents(
|
||||
new Set(agents.map((agent) => agent.agent_id)),
|
||||
);
|
||||
} else {
|
||||
setSelectedAgents(new Set());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="font-sans text-sm font-medium text-neutral-800">
|
||||
Agent info
|
||||
</TableHead>
|
||||
<TableHead className="font-sans text-sm font-medium text-neutral-800">
|
||||
Date submitted
|
||||
</TableHead>
|
||||
<TableHead className="font-sans text-sm font-medium text-neutral-800">
|
||||
Status
|
||||
</TableHead>
|
||||
<TableHead className="font-sans text-sm font-medium text-neutral-800">
|
||||
Runs
|
||||
</TableHead>
|
||||
<TableHead className="text-right font-sans text-sm font-medium text-neutral-800">
|
||||
Reviews
|
||||
</TableHead>
|
||||
<TableHead className="font-sans text-sm font-medium text-neutral-800"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{agents.length > 0 ? (
|
||||
agents.map((agent) => (
|
||||
<AgentTableRow
|
||||
key={agent.id}
|
||||
{...agent}
|
||||
selectedAgents={selectedAgents}
|
||||
setSelectedAgents={setSelectedAgents}
|
||||
onEditSubmission={onEditSubmission}
|
||||
onDeleteSubmission={onDeleteSubmission}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="py-4 text-center">
|
||||
<span className="font-sans text-base text-zinc-600 dark:text-neutral-400">
|
||||
No agents available. Create your first agent to get started!
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Table body */}
|
||||
{agents.length > 0 ? (
|
||||
<div className="flex flex-col">
|
||||
{agents.map((agent, index) => (
|
||||
<div key={agent.id} className="md:block">
|
||||
<AgentTableRow
|
||||
{/* Mobile view with cards */}
|
||||
<div className="md:hidden">
|
||||
{agents.length > 0 ? (
|
||||
<div className="flex flex-col">
|
||||
{agents.map((agent) => (
|
||||
<AgentTableCard
|
||||
key={agent.id}
|
||||
{...agent}
|
||||
selectedAgents={selectedAgents}
|
||||
setSelectedAgents={setSelectedAgents}
|
||||
onEditSubmission={onEditSubmission}
|
||||
onDeleteSubmission={onDeleteSubmission}
|
||||
/>
|
||||
<div className="block md:hidden">
|
||||
<AgentTableCard
|
||||
{...agent}
|
||||
onEditSubmission={onEditSubmission}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-4 text-center font-sans text-base text-neutral-600 dark:text-neutral-400">
|
||||
No agents available. Create your first agent to get started!
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-4 text-center font-sans text-base text-neutral-600 dark:text-neutral-400">
|
||||
No agents available. Create your first agent to get started!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { AgentTableCard } from "./AgentTableCard";
|
||||
import { userEvent, within, expect } from "@storybook/test";
|
||||
import { type StatusType } from "./Status";
|
||||
|
||||
const meta: Meta<typeof AgentTableCard> = {
|
||||
title: "AGPT UI/Agent Table Card",
|
||||
component: AgentTableCard,
|
||||
tags: ["autodocs"],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AgentTableCard>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
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" as StatusType,
|
||||
runs: 1500,
|
||||
rating: 4.8,
|
||||
},
|
||||
};
|
||||
|
||||
export const NoRating: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
rating: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const NoRuns: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
runs: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const InactiveAgent: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
status: "INACTIVE" as StatusType,
|
||||
},
|
||||
};
|
||||
|
||||
export const LongDescription: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
description:
|
||||
"This is a very long description that should wrap to multiple lines. It contains detailed information about the agent and its capabilities.",
|
||||
},
|
||||
};
|
||||
|
||||
export const InteractionTest: Story = {
|
||||
...Default,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const moreButton = canvas.getByRole("button");
|
||||
await userEvent.click(moreButton);
|
||||
},
|
||||
};
|
||||
@@ -50,7 +50,10 @@ export const AgentTableCard: React.FC<AgentTableCardProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-b border-neutral-300 p-4 dark:border-neutral-700">
|
||||
<div
|
||||
className="border-b border-neutral-300 p-4 dark:border-neutral-700"
|
||||
data-testid="agent-table-card"
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
<div className="relative h-[56px] w-[100px] overflow-hidden rounded-lg bg-[#d9d9d9] dark:bg-neutral-800">
|
||||
<Image
|
||||
@@ -61,10 +64,10 @@ export const AgentTableCard: React.FC<AgentTableCardProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-[15px] font-medium text-neutral-800 dark:text-neutral-200">
|
||||
<h3 className="font-sans text-sm font-medium text-neutral-600">
|
||||
{agentName}
|
||||
</h3>
|
||||
<p className="line-clamp-2 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
<p className="font-sans text-sm font-normal text-neutral-600">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -76,16 +79,16 @@ export const AgentTableCard: React.FC<AgentTableCardProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-4">
|
||||
<div className="mt-4 flex flex-wrap items-center gap-4">
|
||||
<Status status={status} />
|
||||
<div className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
<div className="font-sans text-sm font-normal text-neutral-600">
|
||||
{dateSubmitted}
|
||||
</div>
|
||||
<div className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
<div className="font-sans text-sm font-normal text-neutral-600">
|
||||
{runs.toLocaleString()} runs
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
|
||||
<span className="font-sans text-sm font-normal text-neutral-600">
|
||||
{rating.toFixed(1)}
|
||||
</span>
|
||||
<IconStarFilled className="h-4 w-4 text-neutral-800 dark:text-neutral-200" />
|
||||
|
||||
@@ -4,9 +4,17 @@ import * as React from "react";
|
||||
import Image from "next/image";
|
||||
import { IconStarFilled, IconMore, IconEdit } from "@/components/ui/icons";
|
||||
import { Status, StatusType } from "./Status";
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import { TrashIcon } from "@radix-ui/react-icons";
|
||||
import { StoreSubmissionRequest } from "@/lib/autogpt-server-api/types";
|
||||
import { TableCell, TableRow } from "../ui/table";
|
||||
import { Checkbox } from "../ui/checkbox";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
|
||||
export interface AgentTableRowProps {
|
||||
agent_id: string;
|
||||
@@ -82,28 +90,22 @@ export const AgentTableRow: React.FC<AgentTableRowProps> = ({
|
||||
}, [agent_id, selectedAgents, setSelectedAgents]);
|
||||
|
||||
return (
|
||||
<div className="hidden items-center border-b border-neutral-300 px-4 py-4 hover:bg-neutral-50 dark:border-neutral-700 dark:hover:bg-neutral-800 md:flex">
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={checkboxId}
|
||||
aria-label={`Select ${agentName}`}
|
||||
className="mr-4 h-5 w-5 rounded border-2 border-neutral-400 dark:border-neutral-600"
|
||||
checked={selectedAgents.has(agent_id)}
|
||||
onChange={handleCheckboxChange}
|
||||
/>
|
||||
{/* Single label instead of multiple */}
|
||||
<label htmlFor={checkboxId} className="sr-only">
|
||||
Select {agentName}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<TableRow className="space-x-2.5 hover:bg-neutral-50 dark:hover:bg-neutral-800">
|
||||
<TableCell className="w-[40px]">
|
||||
<Checkbox
|
||||
id={checkboxId}
|
||||
aria-label={`Select ${agentName}`}
|
||||
checked={selectedAgents.has(agent_id)}
|
||||
onCheckedChange={handleCheckboxChange}
|
||||
/>
|
||||
<label htmlFor={checkboxId} className="sr-only">
|
||||
Select {agentName}
|
||||
</label>
|
||||
</TableCell>
|
||||
|
||||
<div className="grid w-full grid-cols-[minmax(400px,1fr),180px,140px,100px,100px,40px] items-center gap-4">
|
||||
{/* Agent info column */}
|
||||
<TableCell className="max-w-md">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative h-[70px] w-[125px] overflow-hidden rounded-[10px] bg-[#d9d9d9] dark:bg-neutral-700">
|
||||
<div className="relative aspect-video w-[125px] min-w-[125px] overflow-hidden rounded-xl bg-[#d9d9d9] dark:bg-neutral-700">
|
||||
<Image
|
||||
src={imageSrc?.[0] ?? "/nada.png"}
|
||||
alt={agentName}
|
||||
@@ -112,74 +114,66 @@ export const AgentTableRow: React.FC<AgentTableRowProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-[15px] font-medium text-neutral-800 dark:text-neutral-200">
|
||||
<h3 className="line-clamp-2 font-sans text-sm font-medium text-neutral-800">
|
||||
{agentName}
|
||||
</h3>
|
||||
<p className="line-clamp-2 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
<p className="line-clamp-2 font-sans text-sm font-normal text-neutral-600">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{/* Date column */}
|
||||
<div className="pl-14 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{dateSubmitted}
|
||||
</div>
|
||||
<TableCell className="font-sans text-sm font-normal text-neutral-600">
|
||||
{dateSubmitted}
|
||||
</TableCell>
|
||||
|
||||
{/* Status column */}
|
||||
<div>
|
||||
<Status status={status} />
|
||||
</div>
|
||||
<TableCell>
|
||||
<Status status={status} />
|
||||
</TableCell>
|
||||
|
||||
{/* Runs column */}
|
||||
<div className="text-right text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{runs?.toLocaleString() ?? "0"}
|
||||
</div>
|
||||
<TableCell className="text-right font-sans text-sm font-normal text-neutral-600">
|
||||
{runs?.toLocaleString() ?? "-"}
|
||||
</TableCell>
|
||||
|
||||
{/* 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 dark:text-neutral-200">
|
||||
{rating.toFixed(1)}
|
||||
</span>
|
||||
<IconStarFilled className="h-4 w-4 text-neutral-800 dark:text-neutral-200" />
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
No reviews
|
||||
<TableCell className="text-right font-sans text-sm font-normal text-neutral-600">
|
||||
{rating ? (
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
|
||||
{rating.toFixed(1)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<IconStarFilled className="h-4 w-4 text-neutral-800 dark:text-neutral-200" />
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
No reviews
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
{/* Actions - Three dots menu */}
|
||||
<div className="flex justify-end">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<button className="rounded-full p-1 hover:bg-neutral-100 dark:hover:bg-neutral-700">
|
||||
<IconMore className="h-5 w-5 text-neutral-800 dark:text-neutral-200" />
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content className="z-10 rounded-xl border bg-white p-1 shadow-md dark:bg-gray-800">
|
||||
<DropdownMenu.Item
|
||||
onSelect={handleEdit}
|
||||
className="flex cursor-pointer items-center rounded-md px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<IconEdit className="mr-2 h-5 w-5 dark:text-gray-100" />
|
||||
<span className="dark:text-gray-100">Edit</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator className="my-1 h-px bg-gray-300 dark:bg-gray-600" />
|
||||
<DropdownMenu.Item
|
||||
onSelect={handleDelete}
|
||||
className="flex cursor-pointer items-center rounded-md px-3 py-2 text-red-500 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<TrashIcon className="mr-2 h-5 w-5 text-red-500 dark:text-red-400" />
|
||||
<span className="dark:text-red-400">Delete</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TableCell className="text-right font-sans text-sm font-normal text-neutral-600">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger data-testid="dropdown-button">
|
||||
<IconMore className="h-5 w-5 text-neutral-800 dark:text-neutral-200" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="z-10">
|
||||
<DropdownMenuItem onClick={handleEdit}>
|
||||
<div className="flex items-center font-sans text-sm font-normal text-neutral-600">
|
||||
<IconEdit className="mr-2 h-5 w-5" />
|
||||
<span>Edit</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={handleDelete}
|
||||
className="font-sans text-sm font-normal text-red-500"
|
||||
>
|
||||
<TrashIcon className="mr-2 h-5 w-5 text-red-500" />
|
||||
<span>Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import AutogptButton from "./AutogptButton";
|
||||
|
||||
const meta = {
|
||||
title: "Agpt UI/general/AutogptButton",
|
||||
component: AutogptButton,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
} satisfies Meta<typeof AutogptButton>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
children: "Primary Button",
|
||||
variant: "default",
|
||||
},
|
||||
};
|
||||
|
||||
export const PrimaryWithIcon: Story = {
|
||||
args: {
|
||||
icon: true,
|
||||
children: "Primary Button",
|
||||
variant: "default",
|
||||
},
|
||||
};
|
||||
|
||||
export const PrimaryDisabled: Story = {
|
||||
args: {
|
||||
children: "Primary Button",
|
||||
variant: "default",
|
||||
isDisabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
children: "Primary Button",
|
||||
variant: "secondary",
|
||||
},
|
||||
};
|
||||
|
||||
export const SecondaryWithIcon: Story = {
|
||||
args: {
|
||||
icon: true,
|
||||
children: "Secondary Button",
|
||||
variant: "secondary",
|
||||
},
|
||||
};
|
||||
|
||||
export const SecondaryDisabled: Story = {
|
||||
args: {
|
||||
children: "Secondary Button",
|
||||
variant: "secondary",
|
||||
isDisabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Destructive: Story = {
|
||||
args: {
|
||||
children: "Destructive Button",
|
||||
variant: "destructive",
|
||||
},
|
||||
};
|
||||
|
||||
export const DestructiveWithIcon: Story = {
|
||||
args: {
|
||||
icon: true,
|
||||
children: "Destructive Button",
|
||||
variant: "destructive",
|
||||
},
|
||||
};
|
||||
|
||||
export const DestructiveDisabled: Story = {
|
||||
args: {
|
||||
children: "Destructive Button",
|
||||
variant: "destructive",
|
||||
isDisabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Outline: Story = {
|
||||
args: {
|
||||
children: "Outline Button",
|
||||
variant: "outline",
|
||||
},
|
||||
};
|
||||
|
||||
export const OutlineWithIcon: Story = {
|
||||
args: {
|
||||
icon: true,
|
||||
children: "Outline Button",
|
||||
variant: "outline",
|
||||
},
|
||||
};
|
||||
|
||||
export const OutlineDisabled: Story = {
|
||||
args: {
|
||||
children: "Outline Button",
|
||||
variant: "outline",
|
||||
isDisabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Ghost: Story = {
|
||||
args: {
|
||||
children: "Ghost Button",
|
||||
variant: "ghost",
|
||||
},
|
||||
};
|
||||
|
||||
export const GhostDisabled: Story = {
|
||||
args: {
|
||||
children: "Ghost Button",
|
||||
variant: "ghost",
|
||||
isDisabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Link: Story = {
|
||||
args: {
|
||||
children: "Link Button",
|
||||
variant: "link",
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
children: "Loading Button",
|
||||
isLoading: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { PlayIcon, Loader2Icon } from "lucide-react";
|
||||
import { Button } from "../ui/button";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva("inline-flex items-center justify-center", {
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-zinc-700 text-white hover:bg-zinc-800 disabled:bg-zinc-300 border-none",
|
||||
secondary:
|
||||
"bg-zinc-200 text-zinc-800 hover:bg-zinc-300 disabled:bg-zinc-50 disabled:text-zinc-300 border-none",
|
||||
destructive:
|
||||
"bg-red-500 text-white hover:bg-red-600 disabled:bg-zinc-300 disabled:text-white border-none",
|
||||
outline:
|
||||
"bg-white text-zinc-800 hover:bg-zinc-100 border border-zinc-700 disabled:bg-white disabled:border-zinc-300 disabled:text-zinc-300",
|
||||
ghost:
|
||||
"bg-transparent text-zinc-800 hover:bg-zinc-100 disabled:text-zinc-300 disabled:bg-transparent border-none",
|
||||
link: "bg-transparent text-zinc-800 hover:underline border-none hover:bg-transparent",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
interface AutogptButtonProps extends VariantProps<typeof buttonVariants> {
|
||||
icon?: boolean;
|
||||
children: React.ReactNode;
|
||||
isDisabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
[key: string]: any; // Allow any additional props
|
||||
}
|
||||
|
||||
const AutogptButton = ({
|
||||
icon = false,
|
||||
children,
|
||||
variant,
|
||||
isDisabled = false,
|
||||
isLoading = false,
|
||||
...props
|
||||
}: AutogptButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
className={cn(
|
||||
"h-12 space-x-1.5 rounded-[3rem] px-4 py-3 shadow-none",
|
||||
buttonVariants({ variant }),
|
||||
isDisabled && "bg-red-500",
|
||||
isLoading && "bg-[#3F3F4680] text-white",
|
||||
)}
|
||||
disabled={isDisabled || isLoading}
|
||||
variant={variant}
|
||||
{...props}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2Icon className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
icon && <PlayIcon className="h-5 w-5" />
|
||||
)}
|
||||
<p className="font-sans text-sm font-medium">{children}</p>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutogptButton;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import AutogptInput from "./AutogptInput";
|
||||
|
||||
const meta: Meta<typeof AutogptInput> = {
|
||||
title: "Agpt UI/general/AutogptInput",
|
||||
component: AutogptInput,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex items-center justify-center bg-[#E1E1E1] p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
tags: ["autodocs"],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AutogptInput>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
label: "Email",
|
||||
placeholder: "Type something...",
|
||||
onChange: () => {
|
||||
console.log("It's working");
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const IsDisabled: Story = {
|
||||
args: {
|
||||
label: "Email",
|
||||
placeholder: "Type something...",
|
||||
isDisabled: true,
|
||||
onChange: () => {
|
||||
console.log("It's working");
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import { Input } from "../ui/input";
|
||||
import { Label } from "../ui/label";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AutogptInputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
className?: string;
|
||||
label?: string;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
const AutogptInput: React.FC<AutogptInputProps> = ({
|
||||
label,
|
||||
isDisabled,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-2", isDisabled && "opacity-50")}>
|
||||
{label && (
|
||||
<Label className="font-sans text-sm font-medium leading-[1.4rem]">
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
<Input
|
||||
{...props}
|
||||
disabled={isDisabled}
|
||||
className={cn(
|
||||
"m-0 h-10 w-full rounded-3xl border border-zinc-300 bg-white py-2 pl-4 font-sans text-base font-normal text-zinc-800 shadow-none outline-none placeholder:text-zinc-400 focus:border-2 focus:border-[#CBD5E1] focus:shadow-none focus:ring-0",
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutogptInput;
|
||||
@@ -3,15 +3,18 @@ import { BecomeACreator } from "./BecomeACreator";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Become A Creator",
|
||||
title: "Agpt UI/marketing/Become A Creator",
|
||||
component: BecomeACreator,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
title: { control: "text" },
|
||||
description: { control: "text" },
|
||||
buttonText: { control: "text" },
|
||||
onButtonClick: { action: "buttonClicked" },
|
||||
},
|
||||
@@ -22,31 +25,13 @@ type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
title: "Want to contribute?",
|
||||
description: "Join our ever-growing community of hackers and tinkerers",
|
||||
buttonText: "Become a Creator",
|
||||
title: "Become a Creator",
|
||||
buttonText: "Upload your agent",
|
||||
onButtonClick: () => console.log("Button clicked"),
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomText: Story = {
|
||||
args: {
|
||||
title: "Become a Creator Today!",
|
||||
description: "Share your ideas and build amazing AI agents with us",
|
||||
buttonText: "Start Creating",
|
||||
onButtonClick: () => console.log("Custom button clicked"),
|
||||
},
|
||||
};
|
||||
|
||||
export const LongDescription: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
description:
|
||||
"Join our vibrant community of innovators, developers, and AI enthusiasts. Share your unique perspectives, collaborate on groundbreaking projects, and help shape the future of AI technology.",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInteraction: Story = {
|
||||
export const TestingInteractions: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
},
|
||||
|
||||
@@ -2,16 +2,15 @@
|
||||
|
||||
import * as React from "react";
|
||||
import { PublishAgentPopout } from "./composite/PublishAgentPopout";
|
||||
import AutogptButton from "./AutogptButton";
|
||||
interface BecomeACreatorProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
buttonText?: string;
|
||||
onButtonClick?: () => void;
|
||||
}
|
||||
|
||||
export const BecomeACreator: React.FC<BecomeACreatorProps> = ({
|
||||
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,
|
||||
}) => {
|
||||
@@ -20,37 +19,30 @@ export const BecomeACreator: React.FC<BecomeACreatorProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative mx-auto h-auto min-h-[300px] w-full max-w-[1360px] md:min-h-[400px] lg:h-[459px]">
|
||||
<div className="w-full space-y-18 sm:mb-36 md:mb-72">
|
||||
{/* Title */}
|
||||
<h2 className="mb-[77px] font-poppins text-[18px] font-semibold leading-[28px] text-neutral-800 dark:text-neutral-200">
|
||||
<h2 className="font-poppins text-base font-medium text-zinc-500">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
{/* Content Container */}
|
||||
<div className="mx-auto w-full max-w-[900px] px-4 text-center md:px-6 lg:px-0">
|
||||
<h2 className="mb-6 text-center font-poppins text-[48px] font-semibold leading-[54px] tracking-[-0.012em] text-neutral-950 dark:text-neutral-50 md:mb-8 lg:mb-12">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<h2 className="mb-9 text-center font-poppins text-3xl font-semibold leading-[3.5rem] text-neutral-950 md:text-[2.75rem]">
|
||||
Build AI agents and share
|
||||
<br />
|
||||
<span className="text-violet-600 dark:text-violet-400">
|
||||
your
|
||||
</span>{" "}
|
||||
<span className="text-violet-600"> your </span>
|
||||
vision
|
||||
</h2>
|
||||
|
||||
<p className="font-geist mx-auto mb-8 max-w-[90%] text-lg font-normal leading-relaxed text-neutral-700 dark:text-neutral-300 md:mb-10 md:text-xl md:leading-loose lg:mb-14 lg:text-2xl">
|
||||
{description}
|
||||
<p className="mb-12 text-center font-sans text-lg font-normal text-zinc-600">
|
||||
Join a community where your AI creations can inspire, engage, <br />{" "}
|
||||
and be downloaded by users around the world.
|
||||
</p>
|
||||
|
||||
<PublishAgentPopout
|
||||
trigger={
|
||||
<button
|
||||
onClick={handleButtonClick}
|
||||
className="inline-flex h-[48px] cursor-pointer items-center justify-center rounded-[38px] bg-neutral-800 px-8 py-3 transition-colors hover:bg-neutral-700 dark:bg-neutral-700 dark:hover:bg-neutral-600 md:h-[56px] md:px-10 md:py-4 lg:h-[68px] lg:px-12 lg:py-5"
|
||||
>
|
||||
<span className="whitespace-nowrap font-poppins text-base font-medium leading-normal text-neutral-50 md:text-lg md:leading-relaxed lg:text-xl lg:leading-7">
|
||||
{buttonText}
|
||||
</span>
|
||||
</button>
|
||||
<AutogptButton onClick={handleButtonClick}>
|
||||
{buttonText}
|
||||
</AutogptButton>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { BreadCrumbs } from "./BreadCrumbs";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/BreadCrumbs",
|
||||
title: "Agpt UI/marketing/BreadCrumbs",
|
||||
component: BreadCrumbs,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
@@ -48,7 +48,23 @@ export const LongPath: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInteraction: Story = {
|
||||
export const LongNames: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{ name: "Home", link: "/" },
|
||||
{
|
||||
name: "AI-Powered Writing Assistants, AI-Powered Writing Assistants ",
|
||||
link: "/ai-writing-assistants",
|
||||
},
|
||||
{
|
||||
name: "Advanced Grammar and Style Checker",
|
||||
link: "/ai-writing-assistants/grammar-style-checker",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const TestingInteractions: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{ name: "Home", link: "/" },
|
||||
@@ -64,16 +80,3 @@ export const WithInteraction: Story = {
|
||||
await userEvent.click(homeLink);
|
||||
},
|
||||
};
|
||||
|
||||
export const LongNames: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{ name: "Home", link: "/" },
|
||||
{ name: "AI-Powered Writing Assistants", link: "/ai-writing-assistants" },
|
||||
{
|
||||
name: "Advanced Grammar and Style Checker",
|
||||
link: "/ai-writing-assistants/grammar-style-checker",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -13,31 +13,23 @@ interface BreadCrumbsProps {
|
||||
|
||||
export const BreadCrumbs: React.FC<BreadCrumbsProps> = ({ items }) => {
|
||||
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}>
|
||||
<Link href={item.link}>
|
||||
<span className="rounded py-1 pr-2 font-neue 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}
|
||||
</span>
|
||||
</Link>
|
||||
{index < items.length - 1 && (
|
||||
<span className="text-center text-2xl font-normal text-black dark:text-neutral-100">
|
||||
/
|
||||
</span>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex h-auto flex-wrap items-center justify-start gap-2.5 bg-transparent">
|
||||
{items.map((item, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<Link href={item.link}>
|
||||
<span className="font-sans text-base font-medium text-zinc-800 transition-colors duration-200 hover:text-zinc-400 dark:text-neutral-100 dark:hover:text-gray-500">
|
||||
{item.name.length > 50
|
||||
? `${item.name.slice(0, 50)}...`
|
||||
: item.name}
|
||||
</span>
|
||||
</Link>
|
||||
{index < items.length - 1 && (
|
||||
<span className="font-sans text-base font-medium text-zinc-800 dark:text-zinc-100">
|
||||
/
|
||||
</span>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
/**
|
||||
* @deprecated This Button component will be deprecated in the future.
|
||||
* Please use Shadcn Button or the custom AutogptButton component instead.
|
||||
*/
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Button } from "./Button";
|
||||
import { userEvent, within, expect } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Button",
|
||||
title: "Agpt UI/Deprecated/general/Button",
|
||||
component: Button,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
@@ -85,7 +90,7 @@ export const Variants: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const buttons = canvas.getAllByRole("button");
|
||||
await expect(buttons).toHaveLength(6);
|
||||
await expect(buttons).toHaveLength(5);
|
||||
for (const button of buttons) {
|
||||
await userEvent.hover(button);
|
||||
await expect(button).toHaveAttribute(
|
||||
@@ -115,24 +120,6 @@ export const Sizes: Story = {
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const buttons = canvas.getAllByRole("button");
|
||||
await expect(buttons).toHaveLength(5);
|
||||
const sizeClasses = [
|
||||
"h-8 px-3 py-1.5 text-xs",
|
||||
"h-10 px-4 py-2 text-sm",
|
||||
"h-12 px-5 py-2.5 text-lg",
|
||||
"h-10 w-28",
|
||||
"h-10 w-10",
|
||||
];
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
await expect(buttons[i]).toHaveAttribute(
|
||||
"class",
|
||||
expect.stringContaining(sizeClasses[i]),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
|
||||
@@ -28,7 +28,7 @@ const buttonVariants = cva(
|
||||
sm: "h-8 px-3 py-1.5 rounded-full text-xs",
|
||||
lg: "h-12 px-5 py-2.5 rounded-full text-lg",
|
||||
primary:
|
||||
"h-10 w-28 rounded-full sm:h-12 sm:w-32 md:h-[4.375rem] md:w-[11rem] lg:h-[3.125rem] lg:w-[7rem]",
|
||||
"h-10 w-28 rounded-full sm:h-12 sm:w-32 md:h-[4.375rem] md:w-[11rem] lg:h-[3.125rem] lg:w-[7rem] flex items-center justify-center text-xl",
|
||||
icon: "h-10 w-10 justify-center",
|
||||
card: "h-12 p-5 agpt-rounded-card justify-center text-lg",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Chip } from "./Chip";
|
||||
|
||||
const meta: Meta<typeof Chip> = {
|
||||
component: Chip,
|
||||
title: "Agpt UI/general/BasicBadge",
|
||||
argTypes: {
|
||||
children: {
|
||||
control: "text",
|
||||
description: "The content of the badge",
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Chip>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: "Marketing",
|
||||
},
|
||||
};
|
||||
22
autogpt_platform/frontend/src/components/agptui/Chip.tsx
Normal file
22
autogpt_platform/frontend/src/components/agptui/Chip.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Chip({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<Badge
|
||||
className={cn(
|
||||
"rounded-[30px] border border-zinc-400 bg-white px-3.5 py-2 text-center font-sans text-sm font-normal text-zinc-600 shadow-none hover:border-zinc-500 hover:bg-zinc-100 md:text-base",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
@@ -3,11 +3,15 @@ import { CreatorCard } from "./CreatorCard";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Creator Card",
|
||||
title: "Agpt UI/marketing/Creator Card",
|
||||
component: CreatorCard,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
creatorName: { control: "text" },
|
||||
@@ -15,63 +19,65 @@ const meta = {
|
||||
bio: { control: "text" },
|
||||
agentsUploaded: { control: "number" },
|
||||
onClick: { action: "clicked" },
|
||||
key: { control: "number" },
|
||||
},
|
||||
} satisfies Meta<typeof CreatorCard>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const defaultAvatarImage = "testing_avatar.png";
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
index: 0,
|
||||
key: 0,
|
||||
creatorName: "John Doe",
|
||||
creatorImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
creatorImage: defaultAvatarImage,
|
||||
bio: "AI enthusiast and developer with a passion for creating innovative agents.",
|
||||
agentsUploaded: 15,
|
||||
onClick: () => console.log("Default CreatorCard clicked"),
|
||||
},
|
||||
};
|
||||
|
||||
export const NewCreator: Story = {
|
||||
export const NoImage: Story = {
|
||||
args: {
|
||||
index: 1,
|
||||
creatorName: "Jane Smith",
|
||||
creatorImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
bio: "Excited to start my journey in AI agent development!",
|
||||
agentsUploaded: 1,
|
||||
onClick: () => console.log("NewCreator CreatorCard clicked"),
|
||||
key: 0,
|
||||
creatorName: "John Doe",
|
||||
creatorImage: "",
|
||||
bio: "AI enthusiast and developer with a passion for creating innovative agents.",
|
||||
agentsUploaded: 15,
|
||||
onClick: () => console.log("NoImage CreatorCard clicked"),
|
||||
},
|
||||
};
|
||||
|
||||
export const ExperiencedCreator: Story = {
|
||||
export const LongContent: Story = {
|
||||
args: {
|
||||
index: 2,
|
||||
creatorName: "Alex Johnson",
|
||||
creatorImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
bio: "Veteran AI researcher with a focus on natural language processing and machine learning.",
|
||||
agentsUploaded: 50,
|
||||
onClick: () => console.log("ExperiencedCreator CreatorCard clicked"),
|
||||
key: 1,
|
||||
creatorName: "Alexandria Rodriguez-Fitzgerald Johnson III",
|
||||
creatorImage: defaultAvatarImage,
|
||||
bio: "Excited to start my journey in AI agent development! I have a background in computer science and machine learning, with a special interest in creating agents that can assist with everyday tasks and solve complex problems efficiently.",
|
||||
agentsUploaded: 500000,
|
||||
onClick: () => console.log("LongName CreatorCard clicked"),
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInteraction: Story = {
|
||||
export const TestingInteractions: Story = {
|
||||
args: {
|
||||
index: 3,
|
||||
key: 3,
|
||||
creatorName: "Sam Brown",
|
||||
creatorImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
creatorImage: defaultAvatarImage,
|
||||
bio: "Exploring the frontiers of AI and its applications in everyday life.",
|
||||
agentsUploaded: 30,
|
||||
onClick: () => console.log("WithInteraction CreatorCard clicked"),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const creatorCard = canvas.getByText("Sam Brown");
|
||||
const creatorCard = canvas.getByTestId("creator-card");
|
||||
|
||||
// Test hover state
|
||||
await userEvent.hover(creatorCard);
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Test click interaction
|
||||
await userEvent.click(creatorCard);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
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 { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||
|
||||
interface CreatorCardProps {
|
||||
creatorName: string;
|
||||
@@ -14,7 +8,7 @@ interface CreatorCardProps {
|
||||
bio: string;
|
||||
agentsUploaded: number;
|
||||
onClick: () => void;
|
||||
index: number;
|
||||
key: number;
|
||||
}
|
||||
|
||||
export const CreatorCard: React.FC<CreatorCardProps> = ({
|
||||
@@ -23,43 +17,38 @@ export const CreatorCard: React.FC<CreatorCardProps> = ({
|
||||
bio,
|
||||
agentsUploaded,
|
||||
onClick,
|
||||
index,
|
||||
key,
|
||||
}) => {
|
||||
const backgroundColor = BACKGROUND_COLORS[index % BACKGROUND_COLORS.length];
|
||||
|
||||
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`}
|
||||
key={key}
|
||||
className={`aspect-square w-full space-y-4 rounded-3xl bg-amber-100 p-5 pt-6 hover:cursor-pointer hover:bg-amber-200 sm:w-80`}
|
||||
onClick={onClick}
|
||||
data-testid="creator-card"
|
||||
>
|
||||
<div className="relative h-[64px] w-[64px]">
|
||||
<div className="absolute inset-0 overflow-hidden rounded-full">
|
||||
{creatorImage ? (
|
||||
<Image
|
||||
src={creatorImage}
|
||||
alt={creatorName}
|
||||
width={64}
|
||||
height={64}
|
||||
className="h-full w-full object-cover"
|
||||
priority
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full w-full bg-neutral-300 dark:bg-neutral-600" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Avatar className="h-[84px] w-[84px]">
|
||||
<AvatarImage
|
||||
width={84}
|
||||
height={84}
|
||||
src={creatorImage}
|
||||
alt={`${creatorName}`}
|
||||
/>
|
||||
<AvatarFallback size={84} className="h-[84px] w-[84px]">
|
||||
{creatorName.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="font-poppins text-2xl font-semibold leading-tight text-neutral-900 dark:text-neutral-100">
|
||||
<div className="flex h-36 flex-col gap-2">
|
||||
<h3 className="line-clamp-1 font-poppins text-3xl font-medium text-zinc-800 dark:text-neutral-100">
|
||||
{creatorName}
|
||||
</h3>
|
||||
<p className="font-geist text-sm font-normal leading-normal text-neutral-600 dark:text-neutral-400">
|
||||
<p className="line-clamp-3 font-sans text-base font-normal text-zinc-600 dark:text-neutral-400">
|
||||
{bio}
|
||||
</p>
|
||||
<div className="font-geist text-lg font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
|
||||
{agentsUploaded} agents
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="font-sans text-sm font-medium text-zinc-800 dark:text-neutral-200">
|
||||
{agentsUploaded} agents
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,11 +2,15 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { CreatorInfoCard } from "./CreatorInfoCard";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Creator Info Card",
|
||||
title: "Agpt UI/marketing/Creator Info Card",
|
||||
component: CreatorInfoCard,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
username: { control: "text" },
|
||||
@@ -32,24 +36,24 @@ export const Default: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const NewCreator: Story = {
|
||||
export const LongContent: Story = {
|
||||
args: {
|
||||
username: "AI Enthusiast",
|
||||
handle: "ai_newbie",
|
||||
username: "This Is An Extremel Long Username To Test",
|
||||
handle: "this_is_an_extremely_long_there_what",
|
||||
avatarSrc: "https://example.com/avatar2.jpg",
|
||||
categories: ["AI", "Technology"],
|
||||
averageRating: 0,
|
||||
totalRuns: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const ExperiencedCreator: Story = {
|
||||
args: {
|
||||
username: "Tech Master",
|
||||
handle: "techmaster",
|
||||
avatarSrc: "https://example.com/avatar3.jpg",
|
||||
categories: ["AI", "Development", "Education"],
|
||||
averageRating: 4.9,
|
||||
totalRuns: 50000,
|
||||
categories: [
|
||||
"Artificial Intelligence",
|
||||
"Machine Learning",
|
||||
"Neural Networks",
|
||||
"Deep Learning",
|
||||
"Natural Language Processing",
|
||||
"Computer Vision",
|
||||
"Robotics",
|
||||
"Data Science",
|
||||
"Cloud Computing",
|
||||
"Internet of Things",
|
||||
],
|
||||
averageRating: 4.8888888888,
|
||||
totalRuns: 1000000000,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as React from "react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { StarRatingIcons } from "@/components/ui/icons";
|
||||
import { Separator } from "../ui/separator";
|
||||
import { Chip } from "./Chip";
|
||||
|
||||
interface CreatorInfoCardProps {
|
||||
username: string;
|
||||
@@ -21,90 +23,86 @@ export const CreatorInfoCard: React.FC<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"
|
||||
className="h-auto w-full max-w-md space-y-6 overflow-hidden rounded-[26px] bg-violet-100 px-5 pb-7 pt-6 dark:bg-violet-900 sm:w-[27.5rem]"
|
||||
role="article"
|
||||
aria-label={`Creator profile for ${username}`}
|
||||
>
|
||||
<div className="flex w-full flex-col items-start justify-start gap-3.5 sm:h-[218px]">
|
||||
<Avatar className="h-[100px] w-[100px] sm:h-[130px] sm:w-[130px]">
|
||||
{/* Avatar + Basic Info */}
|
||||
<div className="flex w-full flex-col items-start justify-start gap-3.5">
|
||||
<Avatar className="h-[100px] w-[100px]">
|
||||
<AvatarImage
|
||||
width={130}
|
||||
height={130}
|
||||
width={100}
|
||||
height={100}
|
||||
src={avatarSrc}
|
||||
alt={`${username}'s avatar`}
|
||||
/>
|
||||
<AvatarFallback
|
||||
size={130}
|
||||
className="h-[100px] w-[100px] sm:h-[130px] sm:w-[130px]"
|
||||
>
|
||||
<AvatarFallback size={100} className="h-[100px] w-[100px]">
|
||||
{username.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex w-full flex-col items-start justify-start gap-1.5">
|
||||
<div className="w-full font-poppins text-[35px] font-medium leading-10 text-neutral-900 dark:text-neutral-100 sm:text-[35px] sm:leading-10">
|
||||
<div className="w-full font-poppins text-[1.75rem] font-medium leading-[2.5rem] text-zinc-800 dark:text-zinc-100">
|
||||
{username}
|
||||
</div>
|
||||
<div className="font-geist w-full text-lg font-normal leading-6 text-neutral-800 dark:text-neutral-200 sm:text-xl sm:leading-7">
|
||||
<div className="w-full font-sans text-base font-normal text-zinc-800 dark:text-zinc-200">
|
||||
@{handle}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-4 flex w-full flex-col items-start justify-start gap-6 sm:gap-[50px]">
|
||||
<div className="flex w-full flex-col items-start justify-start gap-3">
|
||||
<div className="h-px w-full bg-neutral-700 dark:bg-neutral-300" />
|
||||
<div className="flex flex-col items-start justify-start gap-2.5">
|
||||
<div className="w-full font-neue text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
Top categories
|
||||
<Separator className="bg-zinc-300" />
|
||||
|
||||
<div className="flex flex-col items-start justify-start gap-4">
|
||||
<div className="w-full font-sans text-sm font-medium text-zinc-800 dark:text-zinc-200 sm:text-base">
|
||||
Top categories
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-wrap items-center gap-2.5"
|
||||
role="list"
|
||||
aria-label="Categories"
|
||||
>
|
||||
{categories.map((category, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-center gap-2.5"
|
||||
role="listitem"
|
||||
>
|
||||
<Chip className="bg-transparent hover:border-zinc-400 hover:bg-transparent">
|
||||
{category}
|
||||
</Chip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="bg-zinc-300" />
|
||||
|
||||
<div className="flex w-full flex-col items-center justify-between gap-4 sm:flex-row sm:gap-0">
|
||||
{/* Average Rating */}
|
||||
<div className="flex w-full flex-col items-start justify-start gap-4">
|
||||
<div className="w-full font-sans text-sm font-medium leading-normal text-zinc-800 dark:text-zinc-200 sm:text-base">
|
||||
Average rating
|
||||
</div>
|
||||
<div className="inline-flex items-center justify-center gap-2">
|
||||
<div className="font-sans text-sm font-medium text-zinc-800 dark:text-zinc-200">
|
||||
{averageRating.toFixed(1)}
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-wrap items-center gap-2.5"
|
||||
role="list"
|
||||
aria-label="Categories"
|
||||
className="flex items-center gap-px"
|
||||
role="img"
|
||||
aria-label={`Rating: ${averageRating} out of 5 stars`}
|
||||
>
|
||||
{categories.map((category, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-center gap-2.5 rounded-[34px] border border-neutral-600 px-4 py-3 dark:border-neutral-400"
|
||||
role="listitem"
|
||||
>
|
||||
<div className="font-neue text-base font-normal leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
{category}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{StarRatingIcons(averageRating)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col items-start justify-start gap-3">
|
||||
<div className="h-px w-full bg-neutral-700 dark:bg-neutral-300" />
|
||||
<div className="flex w-full flex-col items-start justify-between gap-4 sm:flex-row sm:gap-0">
|
||||
<div className="flex w-full flex-col items-start justify-start gap-2.5 sm:w-[164px]">
|
||||
<div className="w-full font-neue text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
Average rating
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<div className="font-geist text-[18px] font-semibold leading-[28px] text-neutral-800 dark:text-neutral-200">
|
||||
{averageRating.toFixed(1)}
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-px"
|
||||
role="img"
|
||||
aria-label={`Rating: ${averageRating} out of 5 stars`}
|
||||
>
|
||||
{StarRatingIcons(averageRating)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-start justify-start gap-2.5 sm:w-[164px]">
|
||||
<div className="w-full font-neue text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
Number of runs
|
||||
</div>
|
||||
<div className="font-geist text-[18px] font-semibold leading-[28px] text-neutral-800 dark:text-neutral-200">
|
||||
{new Intl.NumberFormat().format(totalRuns)} runs
|
||||
</div>
|
||||
</div>
|
||||
{/* Number of runs */}
|
||||
<div className="flex w-full flex-col items-start justify-start gap-4">
|
||||
<div className="w-full font-sans text-sm font-medium leading-normal text-zinc-800 dark:text-zinc-200 sm:text-base">
|
||||
Number of runs
|
||||
</div>
|
||||
<div className="font-sans text-sm font-medium text-zinc-800 dark:text-zinc-200 sm:text-base">
|
||||
{new Intl.NumberFormat().format(totalRuns)} runs
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import CreatorLink from "./CreatorLink";
|
||||
|
||||
const meta: Meta<typeof CreatorLink> = {
|
||||
title: "Agpt UI/marketing/CreatorLink",
|
||||
component: CreatorLink,
|
||||
tags: ["autodocs"],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
argTypes: {
|
||||
href: { control: "text" },
|
||||
children: { control: "text" },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CreatorLink>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
href: "https://linkedin.com",
|
||||
children: "View Creator Profile",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
|
||||
interface CreatorLinkProps {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
key?: number;
|
||||
}
|
||||
|
||||
const CreatorLink: FC<CreatorLinkProps> = ({
|
||||
href,
|
||||
children,
|
||||
className,
|
||||
key,
|
||||
}) => {
|
||||
return (
|
||||
<Link
|
||||
key={key}
|
||||
href={href}
|
||||
className={cn(
|
||||
"flex h-12 w-full min-w-80 max-w-md items-center justify-between rounded-[34px] border border-neutral-600 bg-transparent px-5 py-3",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<p className="font-sans text-base font-medium text-neutral-800">
|
||||
{children}
|
||||
</p>
|
||||
<ExternalLink className="h-5 w-5 stroke-[1.5px]" />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreatorLink;
|
||||
@@ -2,11 +2,15 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { CreatorLinks } from "./CreatorLinks";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Creator Links",
|
||||
title: "Agpt UI/marketing/Creator Links",
|
||||
component: CreatorLinks,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex h-screen w-full items-center justify-center p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
links: {
|
||||
@@ -31,22 +35,6 @@ export const Default: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const WebsiteOnly: Story = {
|
||||
args: {
|
||||
links: ["https://example.com"],
|
||||
},
|
||||
};
|
||||
|
||||
export const SocialLinks: Story = {
|
||||
args: {
|
||||
links: [
|
||||
"https://linkedin.com/in/janedoe",
|
||||
"https://github.com/janedoe",
|
||||
"https://twitter.com/janedoe",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const NoLinks: Story = {
|
||||
args: {
|
||||
links: [],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { getIconForSocial } from "@/components/ui/icons";
|
||||
import CreatorLink from "./CreatorLink";
|
||||
|
||||
interface CreatorLinksProps {
|
||||
links: string[];
|
||||
@@ -10,32 +10,16 @@ export const CreatorLinks: React.FC<CreatorLinksProps> = ({ links }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderLinkButton = (url: string) => (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex min-w-[200px] flex-1 items-center justify-between rounded-[34px] border border-neutral-600 px-5 py-3 dark:border-neutral-400"
|
||||
>
|
||||
<div className="font-neue text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
{new URL(url).hostname.replace("www.", "")}
|
||||
</div>
|
||||
<div className="relative h-6 w-6">
|
||||
{getIconForSocial(url, {
|
||||
className: "h-6 w-6 text-neutral-800 dark:text-neutral-200",
|
||||
})}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start justify-start gap-4">
|
||||
<div className="font-neue text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
<div className="space-y-4">
|
||||
<div className="font-sans text-base font-medium text-zinc-800">
|
||||
Other links
|
||||
</div>
|
||||
<div className="flex w-full flex-wrap gap-3">
|
||||
<div className="grid w-full grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{links.map((link, index) => (
|
||||
<React.Fragment key={index}>{renderLinkButton(link)}</React.Fragment>
|
||||
<CreatorLink href={link} key={index}>
|
||||
{new URL(link).hostname.replace("www.", "").replace(".com", "")}
|
||||
</CreatorLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { useState } from "react";
|
||||
import { StoreAgent } from "@/lib/autogpt-server-api";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||
|
||||
interface FeaturedStoreCardProps {
|
||||
agent: StoreAgent;
|
||||
@@ -27,20 +28,21 @@ export const FeaturedAgentCard: React.FC<FeaturedStoreCardProps> = ({
|
||||
data-testid="featured-store-card"
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className={`flex h-full flex-col ${backgroundColor} rounded-[1.5rem] border-none`}
|
||||
className={`flex h-[30rem] w-full min-w-[94vw] max-w-[27.5rem] flex-col hover:cursor-pointer md:w-[24rem] md:min-w-0 lg:w-[27.5rem] ${backgroundColor} rounded-[1.5rem] border-none px-5 pb-5 pt-6 transition-colors duration-200`}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="line-clamp-2 text-base sm:text-xl">
|
||||
<CardHeader className="mb-7 h-[9.5rem] space-y-3 p-0">
|
||||
<CardTitle className="line-clamp-3 font-poppins text-3xl font-medium text-zinc-800">
|
||||
{agent.agent_name}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm">
|
||||
<CardDescription className="line-clamp-1 font-sans text-base font-normal text-zinc-800">
|
||||
By {agent.creator}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 p-4">
|
||||
<div className="relative aspect-[4/3] w-full overflow-hidden rounded-xl">
|
||||
|
||||
<CardContent className="mb-4 flex flex-1 flex-col gap-4 p-0">
|
||||
<div className="relative flex-1 overflow-hidden rounded-xl">
|
||||
<Image
|
||||
src={agent.agent_image || "/AUTOgpt_Logo_dark.png"}
|
||||
src={agent.agent_image}
|
||||
alt={`${agent.agent_name} preview`}
|
||||
fill
|
||||
sizes="100%"
|
||||
@@ -48,22 +50,41 @@ export const FeaturedAgentCard: React.FC<FeaturedStoreCardProps> = ({
|
||||
isHovered ? "opacity-0" : "opacity-100"
|
||||
}`}
|
||||
/>
|
||||
<Avatar
|
||||
className={`absolute bottom-3 left-3 aspect-square h-[50px] w-[50px] rounded-full border border-zinc-200 transition-opacity duration-200 ${
|
||||
isHovered ? "opacity-0" : "opacity-100"
|
||||
}`}
|
||||
>
|
||||
<AvatarImage
|
||||
width={50}
|
||||
height={50}
|
||||
src={agent.creator_avatar}
|
||||
alt={`${agent.creator_avatar} avatar`}
|
||||
/>
|
||||
<AvatarFallback size={50}>
|
||||
{agent.creator_avatar.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div
|
||||
className={`absolute inset-0 overflow-y-auto p-4 transition-opacity duration-200 ${
|
||||
className={`absolute inset-0 overflow-hidden p-0 transition-opacity duration-200 ${
|
||||
isHovered ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
>
|
||||
<CardDescription className="line-clamp-[6] text-xs sm:line-clamp-[8] sm:text-sm">
|
||||
<CardDescription
|
||||
data-testid="agent-description"
|
||||
className="line-clamp-6 font-sans text-sm text-zinc-600"
|
||||
>
|
||||
{agent.description}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex items-center justify-between">
|
||||
<div className="font-semibold">
|
||||
<CardFooter className="flex min-h-7 flex-col items-start justify-between p-0 sm:flex-row sm:items-center">
|
||||
<div className="font-sans text-base font-medium text-zinc-800">
|
||||
{agent.runs?.toLocaleString() ?? "0"} runs
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-1.5 font-sans text-base font-medium text-zinc-800">
|
||||
<p>{agent.rating.toFixed(1) ?? "0.0"}</p>
|
||||
{StarRatingIcons(agent.rating)}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { FeaturedAgentCard } from "./FeaturedAgentCard";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
import { userEvent, within, expect } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Featured Store Card",
|
||||
title: "Agpt UI/marketing/Featured Store Card",
|
||||
component: FeaturedAgentCard,
|
||||
parameters: {
|
||||
layout: {
|
||||
@@ -11,6 +11,13 @@ const meta = {
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
agent: {
|
||||
@@ -32,50 +39,118 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const BACKGROUND_COLORS = [
|
||||
"bg-violet-100 hover:bg-violet-200 dark:bg-violet-800",
|
||||
"bg-blue-100 hover:bg-blue-200 dark:bg-blue-800",
|
||||
"bg-green-100 hover:bg-green-200 dark:bg-green-800",
|
||||
];
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
agent: {
|
||||
slug: "ai-writing-assistant",
|
||||
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.",
|
||||
"Transform ideas into breathtaking images with this AI-powered Image Generator.",
|
||||
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: {
|
||||
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",
|
||||
creator_avatar: "testing_avatar.png",
|
||||
creator: "John Ababesh",
|
||||
runs: 200000,
|
||||
rating: 4.6,
|
||||
},
|
||||
backgroundColor: "bg-white",
|
||||
backgroundColor: BACKGROUND_COLORS[0],
|
||||
},
|
||||
};
|
||||
|
||||
export const ExtraLarge: Story = {
|
||||
args: {
|
||||
agent: {
|
||||
agent_name:
|
||||
"Universal Language Translator Pro with Advanced Neural Network Technology and Cross-Cultural Communication Capabilities",
|
||||
sub_heading:
|
||||
"Breaking language barriers with cutting-edge AI translation technology that revolutionizes global communication for businesses and individuals across continents while preserving cultural nuances and contextual meanings",
|
||||
description:
|
||||
"Experience seamless communication across 150+ languages with our advanced neural translation engine. Perfect for international businesses, travelers, and language enthusiasts. Features real-time conversation translation, document processing, and cultural context adaptation to ensure your message is delivered exactly as intended in any language. Our proprietary machine learning algorithms continuously improve translation accuracy with each interaction, adapting to regional dialects and specialized terminology. The system includes voice recognition capabilities, image-to-text translation for signs and documents, and can operate offline in emergency situations where internet connectivity is limited. With dedicated mobile apps for iOS and Android plus browser extensions, you'll never encounter language barriers again, whether in business negotiations, academic research, or while exploring new destinations.",
|
||||
agent_image: Default.args.agent.agent_image,
|
||||
creator_avatar: Default.args.agent.creator_avatar,
|
||||
creator:
|
||||
"Global Linguistics Technologies International Corporation and Research Institute for Cross-Cultural Communication",
|
||||
runs: 1000000000,
|
||||
rating: 4.9,
|
||||
slug: "universal-translator-pro-with-advanced-neural-networks-and-multilingual-support-for-global-enterprise-solutions-and-individual-travelers",
|
||||
},
|
||||
backgroundColor: BACKGROUND_COLORS[2],
|
||||
},
|
||||
};
|
||||
|
||||
export const MinimalText: Story = {
|
||||
args: {
|
||||
agent: {
|
||||
agent_name: "A",
|
||||
sub_heading: "B",
|
||||
description: "C",
|
||||
agent_image: Default.args.agent.agent_image,
|
||||
creator_avatar: Default.args.agent.creator_avatar,
|
||||
creator: "D",
|
||||
runs: 0,
|
||||
rating: 0,
|
||||
slug: Default.args.agent.slug,
|
||||
},
|
||||
backgroundColor: BACKGROUND_COLORS[0],
|
||||
},
|
||||
};
|
||||
|
||||
export const MissingImage: Story = {
|
||||
args: {
|
||||
agent: {
|
||||
...Default.args.agent,
|
||||
agent_image: "",
|
||||
},
|
||||
backgroundColor: BACKGROUND_COLORS[1],
|
||||
},
|
||||
};
|
||||
|
||||
export const MissingAvatar: Story = {
|
||||
args: {
|
||||
agent: {
|
||||
...Default.args.agent,
|
||||
creator_avatar: "",
|
||||
},
|
||||
backgroundColor: BACKGROUND_COLORS[2],
|
||||
},
|
||||
};
|
||||
|
||||
export const TestingInteractions: Story = {
|
||||
args: {
|
||||
agent: {
|
||||
...Default.args.agent,
|
||||
runs: 200000,
|
||||
rating: 4.6,
|
||||
},
|
||||
backgroundColor: BACKGROUND_COLORS[1],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const featuredCard = canvas.getByTestId("featured-store-card");
|
||||
await userEvent.hover(featuredCard);
|
||||
await userEvent.click(featuredCard);
|
||||
const card = canvas.getByTestId("featured-store-card");
|
||||
|
||||
await expect(card).toBeInTheDocument();
|
||||
|
||||
await userEvent.hover(card);
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
|
||||
const description = canvas.getByTestId("agent-description");
|
||||
await expect(description).toBeVisible();
|
||||
|
||||
const agentImage = canvas.getByAltText(
|
||||
`${Default.args.agent.agent_name} preview`,
|
||||
);
|
||||
await expect(agentImage).toHaveStyle({ opacity: "0" });
|
||||
|
||||
await userEvent.unhover(card);
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { FilterChips } from "./FilterChips";
|
||||
import { userEvent, within, expect } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Filter Chips",
|
||||
title: "Agpt UI/marketing/Filter Chips",
|
||||
component: FilterChips,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
@@ -23,8 +22,8 @@ const defaultBadges = [
|
||||
"Marketing",
|
||||
"Sales",
|
||||
"Content creation",
|
||||
"Lorem ipsum",
|
||||
"Lorem ipsum",
|
||||
"AI",
|
||||
"Data Science",
|
||||
];
|
||||
|
||||
export const Default: Story = {
|
||||
@@ -41,45 +40,6 @@ export const SingleSelect: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const WithSelectedFilters: Story = {
|
||||
args: {
|
||||
badges: defaultBadges,
|
||||
multiSelect: true,
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const marketingChip = canvas.getByText("Marketing").parentElement;
|
||||
const salesChip = canvas.getByText("Sales").parentElement;
|
||||
if (!marketingChip || !salesChip) {
|
||||
throw new Error("Marketing or Sales chip not found");
|
||||
}
|
||||
|
||||
await userEvent.click(marketingChip);
|
||||
await userEvent.click(salesChip);
|
||||
|
||||
await expect(marketingChip).toHaveClass("bg-neutral-100");
|
||||
await expect(salesChip).toHaveClass("bg-neutral-100");
|
||||
},
|
||||
};
|
||||
|
||||
export const WithFilterChangeCallback: Story = {
|
||||
args: {
|
||||
badges: defaultBadges,
|
||||
multiSelect: true,
|
||||
onFilterChange: (selectedFilters: string[]) => {
|
||||
console.log("Selected filters:", selectedFilters);
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const salesChip = canvas.getByText("Sales");
|
||||
const marketingChip = canvas.getByText("Marketing");
|
||||
|
||||
await userEvent.click(salesChip);
|
||||
await userEvent.click(marketingChip);
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyBadges: Story = {
|
||||
args: {
|
||||
badges: [],
|
||||
@@ -91,33 +51,10 @@ export const LongBadgeNames: Story = {
|
||||
args: {
|
||||
badges: [
|
||||
"Machine Learning",
|
||||
"Natural Language Processing",
|
||||
"Natural Language Processing, Natural Language Processing, Natural Language Processing",
|
||||
"Computer Vision",
|
||||
"Data Science",
|
||||
],
|
||||
multiSelect: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const SingleSelectBehavior: Story = {
|
||||
args: {
|
||||
badges: defaultBadges,
|
||||
multiSelect: false,
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const salesChip = canvas.getByText("Sales").parentElement;
|
||||
const marketingChip = canvas.getByText("Marketing").parentElement;
|
||||
|
||||
if (!salesChip || !marketingChip) {
|
||||
throw new Error("Sales or Marketing chip not found");
|
||||
}
|
||||
|
||||
await userEvent.click(salesChip);
|
||||
await expect(salesChip).toHaveClass("bg-neutral-100");
|
||||
|
||||
await userEvent.click(marketingChip);
|
||||
await expect(marketingChip).toHaveClass("bg-neutral-100");
|
||||
await expect(salesChip).not.toHaveClass("bg-neutral-100");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import * as React from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Chip } from "./Chip";
|
||||
|
||||
interface FilterChipsProps {
|
||||
badges: string[];
|
||||
@@ -36,18 +37,15 @@ export const FilterChips: React.FC<FilterChipsProps> = ({
|
||||
};
|
||||
|
||||
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">
|
||||
<div className="flex flex-wrap items-center justify-center gap-3">
|
||||
{badges.map((badge) => (
|
||||
<Badge
|
||||
<div
|
||||
data-testid="filter-chip"
|
||||
key={badge}
|
||||
variant={selectedFilters.includes(badge) ? "secondary" : "outline"}
|
||||
className="mb-2 flex cursor-pointer items-center justify-center gap-2 rounded-full border border-black/50 px-3 py-1 dark:border-white/50 lg:mb-3 lg:gap-2.5 lg:px-6 lg:py-2"
|
||||
onClick={() => handleBadgeClick(badge)}
|
||||
>
|
||||
<div className="font-neue text-sm font-light tracking-tight text-[#474747] dark:text-[#e0e0e0] lg:text-xl lg:font-medium lg:leading-9">
|
||||
{badge}
|
||||
</div>
|
||||
</Badge>
|
||||
<Chip className="hover:cursor-pointer">{badge}</Chip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { MobileNavBar } from "./MobileNavBar";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
import { IconType } from "../ui/icons";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Mobile Nav Bar",
|
||||
title: "Agpt UI/general/Mobile Nav Bar",
|
||||
component: MobileNavBar,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
viewport: {
|
||||
defaultViewport: "mobile2",
|
||||
},
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
@@ -87,18 +88,3 @@ export const LongUserName: Story = {
|
||||
menuItemGroups: defaultMenuItemGroups,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInteraction: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const menuTrigger = canvas.getByRole("button");
|
||||
|
||||
await userEvent.click(menuTrigger);
|
||||
|
||||
// Wait for the popover to appear
|
||||
await canvas.findByText("Edit profile");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
IconMarketplace,
|
||||
IconLibrary,
|
||||
IconBuilder,
|
||||
IconAutoGPTLogo,
|
||||
} from "../ui/icons";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -125,73 +126,76 @@ export const MobileNavBar: React.FC<MobileNavBarProps> = ({
|
||||
const activeLink = parts.length > 1 ? parts[1] : parts[0];
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
aria-label="Open menu"
|
||||
className="fixed right-4 top-4 z-50 flex h-14 w-14 items-center justify-center rounded-lg border border-neutral-500 bg-neutral-200 hover:bg-gray-200/50 dark:border-neutral-700 dark:bg-neutral-800 dark:hover:bg-gray-700/50 md:hidden"
|
||||
data-testid="mobile-nav-bar-trigger"
|
||||
>
|
||||
{isOpen ? (
|
||||
<IconChevronUp className="h-8 w-8 stroke-black dark:stroke-white" />
|
||||
) : (
|
||||
<IconMenu className="h-8 w-8 stroke-black dark:stroke-white" />
|
||||
)}
|
||||
<span className="sr-only">Open menu</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<AnimatePresence>
|
||||
<PopoverPortal>
|
||||
<Overlay>
|
||||
<PopoverContent asChild>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -32 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -32, transition: { duration: 0.2 } }}
|
||||
className="w-screen rounded-b-2xl bg-white dark:bg-neutral-900"
|
||||
>
|
||||
<div className="mb-4 inline-flex w-full items-end justify-start gap-4">
|
||||
<Avatar className="h-14 w-14 border border-[#474747] dark:border-[#cfcfcf]">
|
||||
<AvatarImage
|
||||
src={avatarSrc}
|
||||
alt={userName || "Unknown User"}
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{userName?.charAt(0) || "U"}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="relative h-14 w-full">
|
||||
<div className="absolute left-0 top-0 text-lg font-semibold leading-7 text-[#474747] dark:text-[#cfcfcf]">
|
||||
{userName || "Unknown User"}
|
||||
</div>
|
||||
<div className="absolute left-0 top-6 font-inter text-base font-normal leading-7 text-[#474747] dark:text-[#cfcfcf]">
|
||||
{userEmail || "No Email Set"}
|
||||
<div className="flex w-full items-center justify-between border-b bg-white/40 px-4 py-2 backdrop-blur-lg">
|
||||
<IconAutoGPTLogo className="h-16 w-16" />
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
aria-label="Open menu"
|
||||
className="z-50 flex h-14 items-center justify-center rounded-lg border-none bg-transparent shadow-none hover:bg-gray-200/50 dark:border-neutral-700 dark:bg-neutral-800 dark:hover:bg-gray-700/50 md:hidden"
|
||||
data-testid="mobile-nav-bar-trigger"
|
||||
>
|
||||
{isOpen ? (
|
||||
<IconChevronUp className="h-5 w-5 stroke-black dark:stroke-white" />
|
||||
) : (
|
||||
<IconMenu className="h-6 w-6 stroke-black dark:stroke-white" />
|
||||
)}
|
||||
<span className="sr-only">Open menu</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<AnimatePresence>
|
||||
<PopoverPortal>
|
||||
<Overlay>
|
||||
<PopoverContent asChild>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -32 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -32, transition: { duration: 0.2 } }}
|
||||
className="w-screen rounded-b-2xl bg-white/40 backdrop-blur-xl dark:bg-neutral-900"
|
||||
>
|
||||
<div className="mb-4 inline-flex w-full items-end justify-start gap-4">
|
||||
<Avatar className="h-14 w-14 self-start dark:border-[#cfcfcf]">
|
||||
<AvatarImage
|
||||
src={avatarSrc}
|
||||
alt={userName || "Unknown User"}
|
||||
/>
|
||||
<AvatarFallback size={56}>
|
||||
{userName?.charAt(0) || "U"}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="relative w-full">
|
||||
<div className="text-lg font-semibold leading-7 text-[#474747] dark:text-[#cfcfcf]">
|
||||
{userName || "Unknown User"}
|
||||
</div>
|
||||
<div className="top-6 font-inter text-base font-normal leading-7 text-[#474747] dark:text-[#cfcfcf]">
|
||||
{userEmail || "No Email Set"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="mb-4 dark:bg-[#3a3a3a]" />
|
||||
{menuItemGroups.map((group, groupIndex) => (
|
||||
<React.Fragment key={groupIndex}>
|
||||
{group.items.map((item, itemIndex) => (
|
||||
<PopoutMenuItem
|
||||
key={itemIndex}
|
||||
icon={item.icon}
|
||||
isActive={item.href === activeLink}
|
||||
text={item.text}
|
||||
onClick={item.onClick}
|
||||
href={item.href}
|
||||
/>
|
||||
))}
|
||||
{groupIndex < menuItemGroups.length - 1 && (
|
||||
<Separator className="my-4 dark:bg-[#3a3a3a]" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</motion.div>
|
||||
</PopoverContent>
|
||||
</Overlay>
|
||||
</PopoverPortal>
|
||||
</AnimatePresence>
|
||||
</Popover>
|
||||
<Separator className="mb-4 dark:bg-[#3a3a3a]" />
|
||||
{menuItemGroups.map((group, groupIndex) => (
|
||||
<React.Fragment key={groupIndex}>
|
||||
{group.items.map((item, itemIndex) => (
|
||||
<PopoutMenuItem
|
||||
key={itemIndex}
|
||||
icon={item.icon}
|
||||
isActive={item.href === activeLink}
|
||||
text={item.text}
|
||||
onClick={item.onClick}
|
||||
href={item.href}
|
||||
/>
|
||||
))}
|
||||
{groupIndex < menuItemGroups.length - 1 && (
|
||||
<Separator className="my-4 dark:bg-[#3a3a3a]" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</motion.div>
|
||||
</PopoverContent>
|
||||
</Overlay>
|
||||
</PopoverPortal>
|
||||
</AnimatePresence>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,10 +3,7 @@ import { Navbar } from "./Navbar";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
import { IconType } from "../ui/icons";
|
||||
import { ProfileDetails } from "@/lib/autogpt-server-api/types";
|
||||
// You can't import this here, jest is not available in storybook and will crash it
|
||||
// import { jest } from "@jest/globals";
|
||||
|
||||
// Mock the API responses
|
||||
const mockProfileData: ProfileDetails = {
|
||||
name: "John Doe",
|
||||
username: "johndoe",
|
||||
@@ -19,22 +16,16 @@ const mockCreditData = {
|
||||
credits: 1500,
|
||||
};
|
||||
|
||||
// Mock the API module
|
||||
// jest.mock("@/lib/autogpt-server-api", () => {
|
||||
// return function () {
|
||||
// return {
|
||||
// getStoreProfile: () => Promise.resolve(mockProfileData),
|
||||
// getUserCredit: () => Promise.resolve(mockCreditData),
|
||||
// };
|
||||
// };
|
||||
// });
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Navbar",
|
||||
title: "Agpt UI/general/Navbar",
|
||||
component: Navbar,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex h-screen w-full justify-center">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
// isLoggedIn: { control: "boolean" },
|
||||
@@ -84,9 +75,19 @@ const defaultMenuItemGroups = [
|
||||
];
|
||||
|
||||
const defaultLinks = [
|
||||
{ name: "Marketplace", href: "/marketplace" },
|
||||
{ name: "Library", href: "/library" },
|
||||
{ name: "Build", href: "/builder" },
|
||||
{
|
||||
name: "Home",
|
||||
href: "/library",
|
||||
},
|
||||
{
|
||||
name: "Marketplace",
|
||||
href: "/marketplace",
|
||||
},
|
||||
|
||||
{
|
||||
name: "Build",
|
||||
href: "/build",
|
||||
},
|
||||
];
|
||||
|
||||
export const Default: Story = {
|
||||
@@ -99,65 +100,3 @@ export const Default: Story = {
|
||||
menuItemGroups: defaultMenuItemGroups,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithActiveLink: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
// activeLink: "/library",
|
||||
},
|
||||
};
|
||||
|
||||
export const LongUserName: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
// avatarSrc: "https://avatars.githubusercontent.com/u/987654321?v=4",
|
||||
},
|
||||
};
|
||||
|
||||
export const NoAvatar: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
// avatarSrc: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInteraction: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const profileTrigger = canvas.getByRole("button");
|
||||
|
||||
await userEvent.click(profileTrigger);
|
||||
|
||||
// Wait for the popover to appear
|
||||
await canvas.findByText("Edit profile");
|
||||
},
|
||||
};
|
||||
|
||||
export const NotLoggedIn: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
// isLoggedIn: false,
|
||||
// avatarSrc: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCredits: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLargeCredits: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithZeroCredits: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,9 +9,9 @@ import { ProfileDetails } from "@/lib/autogpt-server-api/types";
|
||||
import { NavbarLink } from "./NavbarLink";
|
||||
import getServerUser from "@/lib/supabase/getServerUser";
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
|
||||
// Disable theme toggle for now
|
||||
// import { ThemeToggle } from "./ThemeToggle";
|
||||
import MockClient from "@/lib/autogpt-server-api/mock_client";
|
||||
import Image from "next/image";
|
||||
import AutogptButton from "./AutogptButton";
|
||||
|
||||
interface NavLink {
|
||||
name: string;
|
||||
@@ -32,7 +32,7 @@ interface NavbarProps {
|
||||
}
|
||||
|
||||
async function getProfileData() {
|
||||
const api = new BackendAPI();
|
||||
const api = process.env.STORYBOOK ? new MockClient() : new BackendAPI();
|
||||
const profile = await Promise.resolve(api.getStoreProfile());
|
||||
|
||||
return profile;
|
||||
@@ -48,19 +48,28 @@ export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="sticky top-0 z-40 mx-[16px] hidden h-16 items-center justify-between rounded-bl-2xl rounded-br-2xl border border-white/50 bg-white/5 py-3 pl-6 pr-3 backdrop-blur-[26px] dark:border-gray-700 dark:bg-gray-900 md:inline-flex">
|
||||
<div className="flex items-center gap-11">
|
||||
<div className="relative h-10 w-[88.87px]">
|
||||
<IconAutoGPTLogo className="h-full w-full" />
|
||||
</div>
|
||||
<nav className="sticky top-0 z-40 hidden h-16 w-full border-b border-zinc-50 bg-neutral-50/20 px-4 backdrop-blur-[26px] md:flex md:items-center md:justify-center">
|
||||
{/* Nav Links */}
|
||||
<div className="flex flex-1 items-center gap-5">
|
||||
{links.map((link) => (
|
||||
<NavbarLink key={link.name} name={link.name} href={link.href} />
|
||||
))}
|
||||
</div>
|
||||
{/* Profile section */}
|
||||
<div className="flex items-center gap-4">
|
||||
|
||||
{/* Icon */}
|
||||
<Link href="/" className="flex items-center">
|
||||
<Image
|
||||
src="/agpt-logo.svg"
|
||||
alt="AutoGPT Logo"
|
||||
width={90}
|
||||
height={40}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Popouts */}
|
||||
<div className="flex flex-1 items-center justify-end gap-3">
|
||||
{isLoggedIn ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<>
|
||||
{profile && <Wallet />}
|
||||
<ProfilePopoutMenu
|
||||
menuItemGroups={menuItemGroups}
|
||||
@@ -68,25 +77,19 @@ export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => {
|
||||
userEmail={profile?.name}
|
||||
avatarSrc={profile?.avatar_url}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Link href="/login">
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex items-center justify-end space-x-2"
|
||||
>
|
||||
<IconLogIn className="h-5 h-[48px] w-5" />
|
||||
<span>Log In</span>
|
||||
</Button>
|
||||
<AutogptButton variant={"default"}>Log In</AutogptButton>
|
||||
</Link>
|
||||
)}
|
||||
{/* <ThemeToggle /> */}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Mobile Navbar - Adjust positioning */}
|
||||
<>
|
||||
{isLoggedIn ? (
|
||||
<div className="fixed right-4 top-4 z-50">
|
||||
<div className="sticky top-0 z-50 w-full md:hidden">
|
||||
<MobileNavBar
|
||||
userName={profile?.username}
|
||||
menuItemGroups={[
|
||||
|
||||
@@ -1,60 +1,53 @@
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import {
|
||||
IconShoppingCart,
|
||||
IconBoxes,
|
||||
IconLibrary,
|
||||
IconLaptop,
|
||||
} from "@/components/ui/icons";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
interface NavbarLinkProps {
|
||||
name: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
const icons = {
|
||||
"/marketplace": IconShoppingCart,
|
||||
"/build": IconBoxes,
|
||||
"/library": IconLibrary,
|
||||
"/home": IconLibrary,
|
||||
};
|
||||
|
||||
export const NavbarLink = ({ name, href }: NavbarLinkProps) => {
|
||||
const pathname = usePathname();
|
||||
const parts = pathname.split("/");
|
||||
const activeLink = "/" + (parts.length > 2 ? parts[2] : parts[1]);
|
||||
const isActive =
|
||||
href === "/marketplace"
|
||||
? pathname.includes("/marketplace")
|
||||
: pathname === href;
|
||||
|
||||
const Icon = icons[href as keyof typeof icons];
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
data-testid={`navbar-link-${name.toLowerCase()}`}
|
||||
className="font-poppins text-[20px] leading-[28px]"
|
||||
>
|
||||
<Link href={href} data-testid={`navbar-link-${name.toLowerCase()}`}>
|
||||
<div
|
||||
className={`h-[48px] px-5 py-4 ${
|
||||
activeLink === href
|
||||
? "rounded-2xl bg-neutral-800 dark:bg-neutral-200"
|
||||
: ""
|
||||
} flex items-center justify-start gap-3`}
|
||||
className={`flex h-10 items-center justify-start gap-2 px-3 py-2 ${
|
||||
isActive ? "rounded-lg bg-zinc-800 dark:bg-neutral-200" : ""
|
||||
}`}
|
||||
>
|
||||
{href === "/marketplace" && (
|
||||
<IconShoppingCart
|
||||
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
|
||||
/>
|
||||
)}
|
||||
{href === "/build" && (
|
||||
<IconBoxes
|
||||
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
|
||||
/>
|
||||
)}
|
||||
{href === "/monitor" && (
|
||||
<IconLaptop
|
||||
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
|
||||
/>
|
||||
)}
|
||||
{href === "/library" && (
|
||||
<IconLibrary
|
||||
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
|
||||
{Icon && (
|
||||
<Icon
|
||||
className={`h-5 w-5 ${
|
||||
isActive ? "text-zinc-50 dark:text-black" : ""
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`hidden font-poppins text-[20px] font-medium leading-[28px] lg:block ${
|
||||
activeLink === href
|
||||
? "text-neutral-50 dark:text-neutral-900"
|
||||
className={`hidden font-poppins text-base font-medium lg:block ${
|
||||
isActive
|
||||
? "text-zinc-50 dark:text-neutral-900"
|
||||
: "text-neutral-900 dark:text-neutral-50"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { ProfileInfoForm } from "./ProfileInfoForm";
|
||||
|
||||
const meta: Meta<typeof ProfileInfoForm> = {
|
||||
title: "AGPT UI/Profile/Profile Info Form",
|
||||
component: ProfileInfoForm,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
profile: {
|
||||
control: "object",
|
||||
description: "The profile details of the user",
|
||||
displayName: {
|
||||
control: "text",
|
||||
description: "The display name of the user",
|
||||
},
|
||||
handle: {
|
||||
control: "text",
|
||||
description: "The user's handle/username",
|
||||
},
|
||||
bio: {
|
||||
control: "text",
|
||||
description: "User's biography text",
|
||||
},
|
||||
profileImage: {
|
||||
control: "text",
|
||||
description: "URL of the user's profile image",
|
||||
},
|
||||
links: {
|
||||
control: "object",
|
||||
description: "Array of social media links",
|
||||
},
|
||||
categories: {
|
||||
control: "object",
|
||||
description: "Array of selected categories",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ProfileInfoForm>;
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
profile: {
|
||||
name: "",
|
||||
username: "",
|
||||
description: "",
|
||||
avatar_url: "",
|
||||
links: [],
|
||||
top_categories: [],
|
||||
agent_rating: 0,
|
||||
agent_runs: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Filled: Story = {
|
||||
args: {
|
||||
profile: {
|
||||
name: "Olivia Grace",
|
||||
username: "@ograce1421",
|
||||
description:
|
||||
"Our agents are designed to bring happiness and positive vibes to your daily routine. Each template helps you create and live more efficiently.",
|
||||
avatar_url: "https://via.placeholder.com/130x130",
|
||||
links: [
|
||||
"www.websitelink.com",
|
||||
"twitter.com/oliviagrace",
|
||||
"github.com/ograce",
|
||||
],
|
||||
top_categories: ["Entertainment", "Blog", "Content creation"],
|
||||
agent_rating: 4.5,
|
||||
agent_runs: 100,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,22 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { useState, useRef } from "react";
|
||||
|
||||
import Image from "next/image";
|
||||
|
||||
import { Button } from "./Button";
|
||||
import { IconPersonFill } from "@/components/ui/icons";
|
||||
import { CreatorDetails, ProfileDetails } from "@/lib/autogpt-server-api/types";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import useSupabase from "@/hooks/useSupabase";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { Input } from "../ui/input";
|
||||
import { Label } from "../ui/label";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
import AutogptButton from "./AutogptButton";
|
||||
import AutogptInput from "./AutogptInput";
|
||||
|
||||
export const ProfileInfoForm = ({ profile }: { profile: CreatorDetails }) => {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [profileData, setProfileData] = useState(profile);
|
||||
const { supabase } = useSupabase();
|
||||
const api = useBackendAPI();
|
||||
const editPhotoRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const submitForm = async () => {
|
||||
try {
|
||||
@@ -98,171 +103,156 @@ export const ProfileInfoForm = ({ profile }: { profile: CreatorDetails }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full min-w-[800px] px-4 sm:px-8">
|
||||
<h1 className="font-circular mb-6 text-[28px] font-normal text-neutral-900 dark:text-neutral-100 sm:mb-8 sm:text-[35px]">
|
||||
Profile
|
||||
</h1>
|
||||
<div className="mb-8 sm:mb-12">
|
||||
<div className="mb-8 flex flex-col items-center gap-4 sm:flex-row">
|
||||
<div className="flex h-[6.25rem] w-[6.25rem] items-center justify-center rounded-full bg-[#DADADA]">
|
||||
{profileData.avatar_url ? (
|
||||
<Image
|
||||
src={profileData.avatar_url}
|
||||
alt="Profile"
|
||||
fill
|
||||
className="rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<IconPersonFill className="h-10 w-10 text-[#7e7e7e]" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
ref={editPhotoRef}
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
await handleImageUpload(file);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<AutogptButton onClick={() => editPhotoRef.current?.click()}>
|
||||
Edit photo
|
||||
</AutogptButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 sm:mb-12">
|
||||
<div className="mb-8 flex flex-col items-center gap-4 sm:flex-row sm:items-start">
|
||||
<div className="relative h-[130px] w-[130px] rounded-full bg-[#d9d9d9] dark:bg-[#333333]">
|
||||
{profileData.avatar_url ? (
|
||||
<Image
|
||||
src={profileData.avatar_url}
|
||||
alt="Profile"
|
||||
fill
|
||||
className="rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<IconPersonFill className="absolute left-[30px] top-[24px] h-[77.80px] w-[70.63px] text-[#7e7e7e] dark:text-[#999999]" />
|
||||
)}
|
||||
</div>
|
||||
<label className="font-circular mt-11 inline-flex h-[43px] items-center justify-center rounded-[22px] bg-[#15171A] px-6 py-2 text-sm font-normal text-white transition-colors hover:bg-[#2D2F34] dark:bg-white dark:text-[#15171A] dark:hover:bg-[#E5E5E5]">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
await handleImageUpload(file);
|
||||
}
|
||||
<form className="space-y-10" onSubmit={submitForm}>
|
||||
{/* Top section */}
|
||||
<section className="max-w-3xl space-y-6">
|
||||
<AutogptInput
|
||||
label="Display name"
|
||||
type="text"
|
||||
name="displayName"
|
||||
defaultValue={profileData.name}
|
||||
placeholder="Enter your display name"
|
||||
className="h-11 w-full rounded-full border border-[#E2E8F0] px-4 py-2.5 font-inter text-base font-normal text-[#7e7e7e] outline-none"
|
||||
onChange={(e) => {
|
||||
const newProfileData = {
|
||||
...profileData,
|
||||
name: e.target.value,
|
||||
};
|
||||
setProfileData(newProfileData);
|
||||
}}
|
||||
/>
|
||||
|
||||
<AutogptInput
|
||||
label="Handle"
|
||||
type="text"
|
||||
name="handle"
|
||||
defaultValue={profileData.username}
|
||||
placeholder="@username"
|
||||
className="h-11 w-full rounded-full border border-[#E2E8F0] px-4 py-2.5 font-inter text-base font-normal text-[#7e7e7e] outline-none"
|
||||
onChange={(e) => {
|
||||
const newProfileData = {
|
||||
...profileData,
|
||||
username: e.target.value,
|
||||
};
|
||||
setProfileData(newProfileData);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="w-full space-y-1.5">
|
||||
<Label className="font-sans text-sm font-medium leading-[1.4rem]">
|
||||
Bio
|
||||
</Label>
|
||||
<Textarea
|
||||
name="bio"
|
||||
defaultValue={profileData.description}
|
||||
placeholder="Tell us about yourself..."
|
||||
className="m-0 h-10 min-h-56 w-full resize-none rounded-3xl border border-[#E2E8F0] bg-white py-2 pl-4 font-sans text-base font-normal text-zinc-800 shadow-none outline-none ring-0 placeholder:text-zinc-400 focus:border-2 focus:border-[#CBD5E1] focus:shadow-none focus:ring-0"
|
||||
onChange={(e) => {
|
||||
const newProfileData = {
|
||||
...profileData,
|
||||
description: e.target.value,
|
||||
};
|
||||
setProfileData(newProfileData);
|
||||
}}
|
||||
/>
|
||||
Edit photo
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<form className="space-y-4 sm:space-y-6" onSubmit={submitForm}>
|
||||
<div className="w-full">
|
||||
<label className="font-circular mb-1.5 block text-base font-normal leading-tight text-neutral-700 dark:text-neutral-300">
|
||||
Display name
|
||||
</label>
|
||||
<div className="rounded-[55px] border border-slate-200 px-4 py-2.5 dark:border-slate-700 dark:bg-slate-800">
|
||||
<input
|
||||
type="text"
|
||||
name="displayName"
|
||||
defaultValue={profileData.name}
|
||||
placeholder="Enter your display name"
|
||||
className="font-circular w-full border-none bg-transparent text-base font-normal text-neutral-900 placeholder:text-neutral-400 focus:outline-none dark:text-white dark:placeholder:text-neutral-500"
|
||||
onChange={(e) => {
|
||||
const newProfileData = {
|
||||
...profileData,
|
||||
name: e.target.value,
|
||||
};
|
||||
setProfileData(newProfileData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="w-full">
|
||||
<label className="font-circular mb-1.5 block text-base font-normal leading-tight text-neutral-700 dark:text-neutral-300">
|
||||
Handle
|
||||
</label>
|
||||
<div className="rounded-[55px] border border-slate-200 px-4 py-2.5 dark:border-slate-700 dark:bg-slate-800">
|
||||
<input
|
||||
type="text"
|
||||
name="handle"
|
||||
defaultValue={profileData.username}
|
||||
placeholder="@username"
|
||||
className="font-circular w-full border-none bg-transparent text-base font-normal text-neutral-900 placeholder:text-neutral-400 focus:outline-none dark:text-white dark:placeholder:text-neutral-500"
|
||||
onChange={(e) => {
|
||||
const newProfileData = {
|
||||
...profileData,
|
||||
username: e.target.value,
|
||||
};
|
||||
setProfileData(newProfileData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="bg-neutral-300" />
|
||||
|
||||
<div className="w-full">
|
||||
<label className="font-circular mb-1.5 block text-base font-normal leading-tight text-neutral-700 dark:text-neutral-300">
|
||||
Bio
|
||||
</label>
|
||||
<div className="h-[220px] rounded-2xl border border-slate-200 py-2.5 pl-4 pr-4 dark:border-slate-700 dark:bg-slate-800">
|
||||
<textarea
|
||||
name="bio"
|
||||
defaultValue={profileData.description}
|
||||
placeholder="Tell us about yourself..."
|
||||
className="font-circular h-full w-full resize-none border-none bg-transparent text-base font-normal text-neutral-900 placeholder:text-neutral-400 focus:outline-none dark:text-white dark:placeholder:text-neutral-500"
|
||||
onChange={(e) => {
|
||||
const newProfileData = {
|
||||
...profileData,
|
||||
description: e.target.value,
|
||||
};
|
||||
setProfileData(newProfileData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="font-circular mb-4 text-lg font-normal leading-7 text-neutral-700 dark:text-neutral-300">
|
||||
{/* mid section */}
|
||||
<section className="mb-8 max-w-3xl space-y-6">
|
||||
<div>
|
||||
<h2 className="font-poppins text-base font-medium text-neutral-900">
|
||||
Your links
|
||||
</h2>
|
||||
<p className="font-circular mb-6 text-base font-normal leading-tight text-neutral-600 dark:text-neutral-400">
|
||||
<p className="font-sans text-sm font-normal text-zinc-800">
|
||||
You can display up to 5 links on your profile
|
||||
</p>
|
||||
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
{[1, 2, 3, 4, 5].map((linkNum) => {
|
||||
const link = profileData.links[linkNum - 1];
|
||||
return (
|
||||
<div key={linkNum} className="w-full">
|
||||
<label className="font-circular mb-1.5 block text-base font-normal leading-tight text-neutral-700 dark:text-neutral-300">
|
||||
Link {linkNum}
|
||||
</label>
|
||||
<div className="rounded-[55px] border border-slate-200 px-4 py-2.5 dark:border-slate-700 dark:bg-slate-800">
|
||||
<input
|
||||
type="text"
|
||||
name={`link${linkNum}`}
|
||||
placeholder="https://"
|
||||
defaultValue={link || ""}
|
||||
className="font-circular w-full border-none bg-transparent text-base font-normal text-neutral-900 placeholder:text-neutral-400 focus:outline-none dark:text-white dark:placeholder:text-neutral-500"
|
||||
onChange={(e) => {
|
||||
const newLinks = [...profileData.links];
|
||||
newLinks[linkNum - 1] = e.target.value;
|
||||
const newProfileData = {
|
||||
...profileData,
|
||||
links: newLinks,
|
||||
};
|
||||
setProfileData(newProfileData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex h-[50px] items-center justify-end gap-3 py-8">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="font-circular h-[50px] rounded-[35px] bg-neutral-200 px-6 py-3 text-base font-medium text-neutral-800 transition-colors hover:bg-neutral-300 dark:border-neutral-700 dark:bg-neutral-700 dark:text-neutral-200 dark:hover:border-neutral-600 dark:hover:bg-neutral-600"
|
||||
onClick={() => {
|
||||
setProfileData(profile);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="font-circular h-[50px] rounded-[35px] bg-neutral-800 px-6 py-3 text-base font-medium text-white transition-colors hover:bg-neutral-900 dark:bg-neutral-200 dark:text-neutral-900 dark:hover:bg-neutral-100"
|
||||
onClick={submitForm}
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
{[1, 2, 3, 4, 5].map((linkNum) => {
|
||||
const link = profileData.links[linkNum - 1];
|
||||
return (
|
||||
<AutogptInput
|
||||
key={linkNum}
|
||||
label={`Link ${linkNum}`}
|
||||
type="text"
|
||||
name={`link${linkNum}`}
|
||||
placeholder="https://"
|
||||
defaultValue={link || ""}
|
||||
className="h-11 w-full rounded-full border border-[#E2E8F0] px-4 py-2.5 font-inter text-base font-normal text-[#7e7e7e] outline-none"
|
||||
onChange={(e) => {
|
||||
const newLinks = [...profileData.links];
|
||||
newLinks[linkNum - 1] = e.target.value;
|
||||
const newProfileData = {
|
||||
...profileData,
|
||||
links: newLinks,
|
||||
};
|
||||
setProfileData(newProfileData);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* buttons */}
|
||||
<section className="flex h-[50px] items-center justify-end gap-3 py-8">
|
||||
<AutogptButton
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="h-[50px] rounded-[35px] bg-neutral-200 px-6 py-3 font-sans text-base font-medium text-neutral-800 transition-colors hover:bg-neutral-300 dark:border-neutral-700 dark:bg-neutral-700 dark:text-neutral-200 dark:hover:border-neutral-600 dark:hover:bg-neutral-600"
|
||||
onClick={() => {
|
||||
setProfileData(profile);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</AutogptButton>
|
||||
<AutogptButton
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="h-[50px] rounded-[35px] bg-neutral-800 px-6 py-3 font-sans text-base font-medium text-white transition-colors hover:bg-neutral-900 dark:bg-neutral-200 dark:text-neutral-900 dark:hover:bg-neutral-100"
|
||||
onClick={submitForm}
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Save changes"}
|
||||
</AutogptButton>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { ProfilePopoutMenu } from "./ProfilePopoutMenu";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
import { IconType } from "../ui/icons";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Profile Popout Menu",
|
||||
component: ProfilePopoutMenu,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
userName: { control: "text" },
|
||||
userEmail: { control: "text" },
|
||||
avatarSrc: { control: "text" },
|
||||
menuItemGroups: { control: "object" },
|
||||
hideNavBarUsername: { control: "boolean" },
|
||||
},
|
||||
} satisfies Meta<typeof ProfilePopoutMenu>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const defaultMenuItemGroups = [
|
||||
{
|
||||
// Creator actions group
|
||||
items: [
|
||||
{
|
||||
icon: IconType.LayoutDashboard,
|
||||
text: "Creator Dashboard",
|
||||
href: "/dashboard",
|
||||
},
|
||||
{
|
||||
icon: IconType.UploadCloud,
|
||||
text: "Publish an agent",
|
||||
href: "/publish",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// Profile management group
|
||||
items: [
|
||||
{ icon: IconType.Edit, text: "Edit profile", href: "/profile/edit" },
|
||||
{ icon: IconType.Settings, text: "Settings", href: "/settings" },
|
||||
],
|
||||
},
|
||||
{
|
||||
// Logout group
|
||||
items: [
|
||||
{
|
||||
icon: IconType.LogOut,
|
||||
text: "Log out",
|
||||
onClick: () => console.log("Logged out"),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
userName: "John Doe",
|
||||
userEmail: "john.doe@example.com",
|
||||
avatarSrc: "https://avatars.githubusercontent.com/u/123456789?v=4",
|
||||
menuItemGroups: defaultMenuItemGroups,
|
||||
},
|
||||
};
|
||||
|
||||
export const NoAvatar: Story = {
|
||||
args: {
|
||||
userName: "Jane Smith",
|
||||
userEmail: "jane.smith@example.com",
|
||||
menuItemGroups: defaultMenuItemGroups,
|
||||
},
|
||||
};
|
||||
|
||||
export const LongUserName: Story = {
|
||||
args: {
|
||||
userName: "Alexander Bartholomew Christopherson III",
|
||||
userEmail: "alexander@example.com",
|
||||
avatarSrc: "https://avatars.githubusercontent.com/u/987654321?v=4",
|
||||
menuItemGroups: defaultMenuItemGroups,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInteraction: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const profileTrigger = canvas.getByText("John Doe");
|
||||
|
||||
await userEvent.click(profileTrigger);
|
||||
|
||||
// Wait for the popover to appear
|
||||
await canvas.findByText("Edit profile");
|
||||
},
|
||||
};
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import Link from "next/link";
|
||||
import { ProfilePopoutMenuLogoutButton } from "./ProfilePopoutMenuLogoutButton";
|
||||
import { PublishAgentPopout } from "./composite/PublishAgentPopout";
|
||||
import AutogptButton from "./AutogptButton";
|
||||
|
||||
interface ProfilePopoutMenuProps {
|
||||
userName?: string;
|
||||
@@ -72,9 +73,9 @@ export const ProfilePopoutMenu: React.FC<ProfilePopoutMenuProps> = ({
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
<AutogptButton
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center space-x-3"
|
||||
variant={"link"}
|
||||
aria-label="Open profile menu"
|
||||
aria-controls={popupId}
|
||||
aria-haspopup="true"
|
||||
@@ -86,26 +87,26 @@ export const ProfilePopoutMenu: React.FC<ProfilePopoutMenuProps> = ({
|
||||
{userName?.charAt(0) || "U"}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</button>
|
||||
</AutogptButton>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
id={popupId}
|
||||
className="flex h-[380px] w-[300px] flex-col items-start justify-start gap-4 rounded-[26px] bg-zinc-400/70 p-6 shadow backdrop-blur-2xl dark:bg-zinc-800/70"
|
||||
className="mr-8 flex w-[300px] flex-col items-start justify-start gap-4 rounded-[26px] bg-zinc-400/70 p-6 shadow backdrop-blur-2xl dark:bg-zinc-800/70"
|
||||
>
|
||||
{/* Header with avatar and user info */}
|
||||
<div className="inline-flex items-center justify-start gap-4 self-stretch">
|
||||
<Avatar className="h-[60px] w-[60px]">
|
||||
<Avatar className="h-16 w-16">
|
||||
<AvatarImage src={avatarSrc} alt="" aria-hidden="true" />
|
||||
<AvatarFallback aria-hidden="true">
|
||||
{userName?.charAt(0) || "U"}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="relative h-[47px] w-[173px]">
|
||||
<div className="absolute left-0 top-0 font-sans text-base font-semibold leading-7 text-white dark:text-neutral-200">
|
||||
<div>
|
||||
<div className="font-sans text-base font-semibold leading-7 text-white dark:text-neutral-200">
|
||||
{userName}
|
||||
</div>
|
||||
<div className="absolute left-0 top-[23px] font-sans text-base font-normal leading-normal text-white dark:text-neutral-400">
|
||||
<div className="font-sans text-base font-normal leading-normal text-white dark:text-neutral-400">
|
||||
{userEmail}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,16 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { PublishAgentAwaitingReview } from "./PublishAgentAwaitingReview";
|
||||
|
||||
const meta: Meta<typeof PublishAgentAwaitingReview> = {
|
||||
title: "AGPT UI/Publish Agent Awaiting Review",
|
||||
title: "Agpt UI/marketing/Publish Agent Awaiting Review",
|
||||
component: PublishAgentAwaitingReview,
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="backdrop-blur-4 flex h-screen items-center justify-center bg-black/40 md:p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import * as React from "react";
|
||||
import { IconClose } from "../ui/icons";
|
||||
import Image from "next/image";
|
||||
import { Button } from "../agptui/Button";
|
||||
import { Button } from "./Button";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
interface PublishAgentAwaitingReviewProps {
|
||||
agentName: string;
|
||||
@@ -28,37 +29,35 @@ export const PublishAgentAwaitingReview: React.FC<
|
||||
}) => {
|
||||
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"
|
||||
role="dialog"
|
||||
aria-labelledby="modal-title"
|
||||
className="m-auto flex h-fit w-full max-w-[900px] flex-col rounded-3xl bg-white shadow-lg dark:bg-gray-800"
|
||||
>
|
||||
<div className="relative h-[180px] w-full rounded-none bg-white dark:bg-neutral-800 sm:h-[140px] sm:rounded-t-3xl">
|
||||
<div className="absolute left-0 top-[40px] flex w-full flex-col items-center justify-start px-6 sm:top-[40px]">
|
||||
<div
|
||||
id="modal-title"
|
||||
className="mb-4 text-center font-poppins text-xl font-semibold leading-relaxed text-neutral-900 dark:text-neutral-100 sm:mb-2 sm:text-2xl"
|
||||
{/* Top */}
|
||||
<div className="relative items-center justify-center border-b border-slate-200 pb-4 pt-12 dark:border-slate-700 md:flex md:h-28">
|
||||
<div className="absolute right-4 top-4">
|
||||
<Button
|
||||
onClick={onClose}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full bg-transparent p-0 transition-colors hover:bg-gray-200"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="px-4 text-center">
|
||||
<h3 className="font-poppins text-lg font-semibold text-neutral-900 md:text-2xl">
|
||||
Agent is awaiting review
|
||||
</div>
|
||||
<div className="max-w-[280px] text-center font-inter text-sm font-normal leading-relaxed text-slate-500 dark:text-slate-400 sm:max-w-none">
|
||||
</h3>
|
||||
<p className="hidden font-sans text-sm font-normal text-neutral-600 sm:flex">
|
||||
In the meantime you can check your progress on your Creator
|
||||
Dashboard page
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute right-4 top-4 flex h-[38px] w-[38px] items-center justify-center rounded-full bg-gray-100 transition-colors hover:bg-gray-200 dark:bg-neutral-700 dark:hover:bg-neutral-600"
|
||||
aria-label="Close dialog"
|
||||
>
|
||||
<IconClose
|
||||
size="default"
|
||||
className="text-neutral-600 dark:text-neutral-300"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 flex-col items-center gap-8 px-6 py-6 sm:gap-6">
|
||||
<div className="h-[50vh] flex-grow space-y-5 overflow-y-auto p-4 md:h-[38rem] md:p-6">
|
||||
<div className="mt-4 flex w-full flex-col items-center gap-6 sm:mt-0 sm:gap-4">
|
||||
{/* Heading */}
|
||||
<div className="flex flex-col items-center gap-3 sm:gap-2">
|
||||
<div className="text-center font-sans text-lg font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
|
||||
{agentName}
|
||||
@@ -68,6 +67,7 @@ export const PublishAgentAwaitingReview: React.FC<
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image */}
|
||||
<div
|
||||
className="h-[280px] w-full rounded-xl bg-neutral-200 dark:bg-neutral-700 sm:h-[350px]"
|
||||
role="img"
|
||||
@@ -86,8 +86,9 @@ export const PublishAgentAwaitingReview: React.FC<
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div
|
||||
className="h-[150px] w-full overflow-y-auto font-sans text-base font-normal leading-normal text-neutral-600 dark:text-neutral-400 sm:h-[180px]"
|
||||
className="w-full whitespace-pre-line font-sans text-base font-normal text-neutral-600 dark:text-neutral-400"
|
||||
tabIndex={0}
|
||||
role="region"
|
||||
aria-label="Agent description"
|
||||
@@ -100,13 +101,13 @@ export const PublishAgentAwaitingReview: React.FC<
|
||||
<div className="flex w-full flex-col items-center justify-center gap-4 border-t border-slate-200 p-6 dark:border-slate-700 sm:flex-row">
|
||||
<Button
|
||||
onClick={onDone}
|
||||
className="h-12 w-full rounded-[59px] sm:flex-1"
|
||||
className="flex h-12 w-full items-center justify-center rounded-[59px] sm:flex-1"
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onViewProgress}
|
||||
className="h-12 w-full rounded-[59px] bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-700 dark:text-neutral-100 dark:hover:bg-neutral-600 sm:flex-1"
|
||||
className="flex h-12 w-full items-center justify-center rounded-[59px] bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-700 dark:text-neutral-100 dark:hover:bg-neutral-600 sm:flex-1"
|
||||
>
|
||||
View progress
|
||||
</Button>
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Agent, PublishAgentSelect } from "./PublishAgentSelect";
|
||||
import { userEvent, within, expect } from "@storybook/test";
|
||||
|
||||
const meta: Meta<typeof PublishAgentSelect> = {
|
||||
title: "AGPT UI/Publish Agent Select",
|
||||
title: "Agpt UI/marketing/Publish Agent Select",
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="backdrop-blur-4 flex h-screen items-center justify-center bg-black/40">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
component: PublishAgentSelect,
|
||||
tags: ["autodocs"],
|
||||
};
|
||||
@@ -22,56 +30,56 @@ const mockAgents: Agent[] = [
|
||||
name: "Content Writer",
|
||||
lastEdited: "5 days ago",
|
||||
imageSrc: "https://picsum.photos/seed/writer/300/200",
|
||||
id: "1",
|
||||
id: "2",
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
name: "Data Analyzer",
|
||||
lastEdited: "1 week ago",
|
||||
imageSrc: "https://picsum.photos/seed/data/300/200",
|
||||
id: "1",
|
||||
id: "3",
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
name: "Image Recognition",
|
||||
lastEdited: "2 weeks ago",
|
||||
imageSrc: "https://picsum.photos/seed/image/300/200",
|
||||
id: "1",
|
||||
id: "9",
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
name: "Chatbot Assistant",
|
||||
lastEdited: "3 weeks ago",
|
||||
imageSrc: "https://picsum.photos/seed/chat/300/200",
|
||||
id: "1",
|
||||
id: "4",
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
name: "Code Generator",
|
||||
lastEdited: "1 month ago",
|
||||
imageSrc: "https://picsum.photos/seed/code/300/200",
|
||||
id: "1",
|
||||
id: "5",
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
name: "AI Translator",
|
||||
lastEdited: "6 weeks ago",
|
||||
imageSrc: "https://picsum.photos/seed/translate/300/200",
|
||||
id: "1",
|
||||
id: "6",
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
name: "Voice Assistant",
|
||||
lastEdited: "2 months ago",
|
||||
imageSrc: "https://picsum.photos/seed/voice/300/200",
|
||||
id: "1",
|
||||
id: "7",
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
name: "Data Visualizer",
|
||||
lastEdited: "3 months ago",
|
||||
imageSrc: "https://picsum.photos/seed/visualize/300/200",
|
||||
id: "1",
|
||||
id: "8",
|
||||
version: 1,
|
||||
},
|
||||
];
|
||||
@@ -104,16 +112,20 @@ export const SingleAgent: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const SixAgents: Story = {
|
||||
args: {
|
||||
...defaultArgs,
|
||||
agents: mockAgents.slice(0, 6),
|
||||
},
|
||||
};
|
||||
|
||||
export const NineAgents: Story = {
|
||||
export const TestingInteractions: Story = {
|
||||
args: {
|
||||
...defaultArgs,
|
||||
agents: mockAgents,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Select an agent
|
||||
const agentCard = canvas.getByText("SEO Optimizer");
|
||||
await userEvent.click(agentCard);
|
||||
|
||||
// Click next button
|
||||
const nextButton = canvas.getByText(/next/i);
|
||||
await userEvent.click(nextButton);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as React from "react";
|
||||
import Image from "next/image";
|
||||
import { Button } from "../agptui/Button";
|
||||
import { IconClose } from "../ui/icons";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
export interface Agent {
|
||||
name: string;
|
||||
@@ -48,32 +48,30 @@ export const PublishAgentSelect: React.FC<PublishAgentSelectProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-[900px] flex-col rounded-3xl bg-white shadow-lg dark:bg-gray-800">
|
||||
<div className="relative border-b border-slate-200 p-4 dark:border-slate-700 sm:p-6">
|
||||
<div className="m-auto flex h-fit w-full max-w-[900px] flex-col rounded-3xl bg-white shadow-lg dark:bg-gray-800">
|
||||
{/* Top */}
|
||||
<div className="relative flex h-28 items-center justify-center border-b border-slate-200 dark:border-slate-700">
|
||||
<div className="absolute right-4 top-4">
|
||||
<button
|
||||
<Button
|
||||
onClick={onClose}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-100 transition-colors hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full bg-transparent p-0 transition-colors hover:bg-gray-200"
|
||||
aria-label="Close"
|
||||
>
|
||||
<IconClose
|
||||
size="default"
|
||||
className="text-neutral-600 dark:text-neutral-400"
|
||||
/>
|
||||
</button>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="font-poppins text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
<h3 className="font-poppins text-2xl font-semibold text-neutral-900">
|
||||
Publish Agent
|
||||
</h3>
|
||||
<p className="font-geist text-sm font-normal text-neutral-600 dark:text-neutral-400">
|
||||
<p className="font-sans text-base font-normal text-neutral-600">
|
||||
Select your project that you'd like to publish
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{agents.length === 0 ? (
|
||||
<div className="inline-flex h-[370px] flex-col items-center justify-center gap-[29px] px-4 py-5 sm:px-6">
|
||||
<div className="inline-flex h-96 flex-col items-center justify-center gap-[29px] px-4 py-5 sm:px-6">
|
||||
<div className="w-full text-center font-sans text-lg font-normal leading-7 text-neutral-600 dark:text-neutral-400 sm:w-[573px] sm:text-xl">
|
||||
Uh-oh.. It seems like you don't have any agents in your
|
||||
library.
|
||||
@@ -90,10 +88,10 @@ export const PublishAgentSelect: React.FC<PublishAgentSelectProps> = ({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex-grow overflow-hidden p-4 sm:p-6">
|
||||
<div className="flex-grow overflow-hidden">
|
||||
<h3 className="sr-only">List of agents</h3>
|
||||
<div
|
||||
className="h-[300px] overflow-y-auto pr-2 sm:h-[400px] md:h-[500px]"
|
||||
className="h-72 overflow-y-auto px-6 py-6 sm:h-[400px] md:h-[500px]"
|
||||
role="region"
|
||||
aria-labelledby="agentListHeading"
|
||||
>
|
||||
@@ -135,7 +133,7 @@ export const PublishAgentSelect: React.FC<PublishAgentSelectProps> = ({
|
||||
<p className="font-poppins text-base font-medium leading-normal text-neutral-800 dark:text-neutral-100 sm:text-base">
|
||||
{agent.name}
|
||||
</p>
|
||||
<small className="font-geist text-xs font-normal leading-[14px] text-neutral-500 dark:text-neutral-400 sm:text-sm">
|
||||
<small className="font-sans text-xs font-normal leading-[14px] text-neutral-500 dark:text-neutral-400 sm:text-sm">
|
||||
Edited {agent.lastEdited}
|
||||
</small>
|
||||
</div>
|
||||
@@ -147,7 +145,11 @@ export const PublishAgentSelect: React.FC<PublishAgentSelectProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-4 border-t border-slate-200 p-4 dark:border-slate-700 sm:p-6">
|
||||
<Button onClick={onCancel} size="lg" className="w-full sm:flex-1">
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
size="lg"
|
||||
className="flex w-full items-center justify-center sm:flex-1"
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
@@ -158,7 +160,7 @@ export const PublishAgentSelect: React.FC<PublishAgentSelectProps> = ({
|
||||
}}
|
||||
disabled={!selectedAgentId || !selectedAgentVersion}
|
||||
size="lg"
|
||||
className="w-full bg-neutral-800 text-white hover:bg-neutral-900 sm:flex-1"
|
||||
className="flex w-full items-center justify-center bg-neutral-800 text-white hover:bg-neutral-900 sm:flex-1"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { PublishAgentInfo } from "./PublishAgentSelectInfo";
|
||||
import { expect, userEvent, within } from "@storybook/test";
|
||||
|
||||
const meta: Meta<typeof PublishAgentInfo> = {
|
||||
title: "AGPT UI/Publish Agent Info",
|
||||
title: "Agpt UI/marketing/Publish Agent Select Info",
|
||||
component: PublishAgentInfo,
|
||||
tags: ["autodocs"],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{ maxWidth: "670px", margin: "0 auto" }}>
|
||||
<div className="backdrop-blur-4 flex h-screen items-center justify-center bg-black/40">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
argTypes: {
|
||||
onBack: { action: "back clicked" },
|
||||
onSubmit: { action: "submit clicked" },
|
||||
onClose: { action: "close clicked" },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -23,6 +29,13 @@ export const Default: Story = {
|
||||
onSubmit: () => console.log("Submit clicked"),
|
||||
onClose: () => console.log("Close clicked"),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const titleInput = canvas.getByLabelText(/title/i);
|
||||
|
||||
await userEvent.type(titleInput, "Test Agent");
|
||||
await expect(titleInput).toHaveValue("Test Agent");
|
||||
},
|
||||
};
|
||||
|
||||
export const Filled: Story = {
|
||||
@@ -35,7 +48,7 @@ export const Filled: Story = {
|
||||
subheader: "Boost your website's search engine rankings",
|
||||
thumbnailSrc: "https://picsum.photos/seed/seo/500/350",
|
||||
youtubeLink: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
category: "SEO",
|
||||
category: "marketing",
|
||||
description:
|
||||
"This AI agent specializes in analyzing websites and providing actionable recommendations to improve search engine optimization. It can perform keyword research, analyze backlinks, and suggest content improvements.",
|
||||
},
|
||||
@@ -52,7 +65,7 @@ export const ThreeImages: Story = {
|
||||
subheader: "Showcasing multiple images",
|
||||
thumbnailSrc: "https://picsum.photos/seed/initial/500/350",
|
||||
youtubeLink: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
category: "SEO",
|
||||
category: "marketing",
|
||||
description:
|
||||
"This agent allows you to upload and manage multiple images.",
|
||||
additionalImages: [
|
||||
@@ -63,24 +76,22 @@ export const ThreeImages: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const SixImages: Story = {
|
||||
export const MaxImages: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
initialData: {
|
||||
agent_id: "1",
|
||||
slug: "super-seo-optimizer",
|
||||
title: "Gallery Agent",
|
||||
subheader: "Showcasing a gallery of images",
|
||||
subheader: "Showcasing maximum allowed images",
|
||||
thumbnailSrc: "https://picsum.photos/seed/gallery1/500/350",
|
||||
youtubeLink: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
category: "SEO",
|
||||
description: "This agent displays a gallery of six images.",
|
||||
category: "marketing",
|
||||
description: "This agent displays the maximum number of allowed images.",
|
||||
additionalImages: [
|
||||
"https://picsum.photos/seed/gallery2/500/350",
|
||||
"https://picsum.photos/seed/gallery3/500/350",
|
||||
"https://picsum.photos/seed/gallery4/500/350",
|
||||
"https://picsum.photos/seed/gallery5/500/350",
|
||||
"https://picsum.photos/seed/gallery6/500/350",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,6 +6,17 @@ import { Button } from "../agptui/Button";
|
||||
import { IconClose, IconPlus } from "../ui/icons";
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
import { toast } from "../ui/use-toast";
|
||||
import { X } from "lucide-react";
|
||||
import { Input } from "../ui/input";
|
||||
import { Label } from "../ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../ui/select";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
|
||||
export interface PublishAgentInfoInitialData {
|
||||
agent_id: string;
|
||||
@@ -165,85 +176,88 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full flex-col rounded-3xl bg-white dark:bg-gray-800">
|
||||
<div className="relative p-6">
|
||||
<div className="absolute right-4 top-2">
|
||||
<button
|
||||
<div className="mx-auto flex h-fit w-full max-w-2xl flex-col rounded-3xl bg-white">
|
||||
{/* Top section */}
|
||||
<div className="relative flex h-28 items-center justify-center border-b border-slate-200 dark:border-slate-700">
|
||||
{/* Cancel Button */}
|
||||
<div className="absolute right-4 top-4">
|
||||
<Button
|
||||
onClick={onClose}
|
||||
className="flex h-[38px] w-[38px] items-center justify-center rounded-full bg-gray-100 transition-colors hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full bg-transparent p-0 transition-colors hover:bg-gray-200"
|
||||
aria-label="Close"
|
||||
>
|
||||
<IconClose
|
||||
size="default"
|
||||
className="text-neutral-600 dark:text-neutral-300"
|
||||
/>
|
||||
</button>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div className="text-center">
|
||||
<h3 className="font-poppins text-2xl font-semibold text-neutral-900">
|
||||
Publish Agent
|
||||
</h3>
|
||||
<p className="font-sans text-base font-normal text-neutral-600">
|
||||
Write a bit of details about your agent{" "}
|
||||
</p>
|
||||
</div>
|
||||
<h3 className="h3-poppins text-center text-2xl font-semibold leading-loose text-neutral-900 dark:text-neutral-100">
|
||||
Publish Agent
|
||||
</h3>
|
||||
<p className="p text-center text-base font-normal leading-7 text-neutral-600 dark:text-neutral-400">
|
||||
Write a bit of details about your agent
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow space-y-5 overflow-y-auto p-6">
|
||||
{/* Form fields */}
|
||||
<div className="h-[50vh] flex-grow space-y-5 overflow-y-auto p-4 md:h-[38rem] md:p-6">
|
||||
<div className="space-y-1.5">
|
||||
<label
|
||||
<Label
|
||||
htmlFor="title"
|
||||
className="text-sm font-medium leading-tight text-slate-950 dark:text-slate-300"
|
||||
className="font-sans text-sm font-medium text-[#020617]"
|
||||
>
|
||||
Title
|
||||
</label>
|
||||
<input
|
||||
</Label>
|
||||
<Input
|
||||
id="title"
|
||||
type="text"
|
||||
placeholder="Agent name"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
className="p-ui-medium w-full rounded-[55px] border border-slate-200 py-2.5 pl-4 pr-14 text-base font-normal leading-normal text-slate-500 dark:border-slate-700 dark:bg-gray-700 dark:text-slate-300"
|
||||
className="h-11 rounded-full border border-[#E2E8F0] px-4 py-2.5 font-sans text-sm text-neutral-500 md:text-base"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label
|
||||
<Label
|
||||
htmlFor="subheader"
|
||||
className="text-sm font-medium leading-tight text-slate-950 dark:text-slate-300"
|
||||
className="font-sans text-sm font-medium text-[#020617]"
|
||||
>
|
||||
Subheader
|
||||
</label>
|
||||
<input
|
||||
</Label>
|
||||
<Input
|
||||
id="subheader"
|
||||
type="text"
|
||||
placeholder="A tagline for your agent"
|
||||
value={subheader}
|
||||
onChange={(e) => setSubheader(e.target.value)}
|
||||
className="w-full rounded-[55px] border border-slate-200 py-2.5 pl-4 pr-14 font-sans text-base font-normal leading-normal text-slate-500 dark:border-slate-700 dark:bg-gray-700 dark:text-slate-300"
|
||||
className="h-11 rounded-full border border-[#E2E8F0] px-4 py-2.5 font-sans text-sm text-neutral-500 md:text-base"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label
|
||||
<Label
|
||||
htmlFor="slug"
|
||||
className="text-sm font-medium leading-tight text-slate-950 dark:text-slate-300"
|
||||
className="font-sans text-sm font-medium text-[#020617]"
|
||||
>
|
||||
Slug
|
||||
</label>
|
||||
<input
|
||||
</Label>
|
||||
<Input
|
||||
id="slug"
|
||||
type="text"
|
||||
placeholder="URL-friendly name for your agent"
|
||||
value={slug}
|
||||
onChange={(e) => setSlug(e.target.value)}
|
||||
className="w-full rounded-[55px] border border-slate-200 py-2.5 pl-4 pr-14 font-sans text-base font-normal leading-normal text-slate-500 dark:border-slate-700 dark:bg-gray-700 dark:text-slate-300"
|
||||
className="h-11 rounded-full border border-[#E2E8F0] px-4 py-2.5 font-sans text-sm text-neutral-500 md:text-base"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2.5">
|
||||
<label className="text-sm font-medium leading-tight text-slate-950 dark:text-slate-300">
|
||||
<Label className="font-sans text-sm font-medium text-[#020617]">
|
||||
Thumbnail images
|
||||
</label>
|
||||
<div className="flex h-[350px] items-center justify-center overflow-hidden rounded-[20px] border border-neutral-300 p-2.5 dark:border-neutral-600">
|
||||
</Label>
|
||||
<div className="flex h-[350px] items-center justify-center overflow-hidden rounded-[20px] border border-dashed border-neutral-300 p-2.5 dark:border-neutral-600">
|
||||
{selectedImage !== null && selectedImage !== undefined ? (
|
||||
<Image
|
||||
src={selectedImage}
|
||||
@@ -268,10 +282,13 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
|
||||
<Button
|
||||
onClick={handleAddImage}
|
||||
variant="ghost"
|
||||
className="flex h-[70px] w-[100px] flex-col items-center justify-center rounded-md bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-700 dark:hover:bg-neutral-600"
|
||||
className="h-[70px] w-[100px] flex-col items-center justify-center rounded-md bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-700 dark:hover:bg-neutral-600"
|
||||
>
|
||||
<label htmlFor="image-upload" className="cursor-pointer">
|
||||
<input
|
||||
<Label
|
||||
htmlFor="image-upload"
|
||||
className="flex flex-col items-center justify-center font-sans text-sm font-medium text-[#020617]"
|
||||
>
|
||||
<Input
|
||||
id="image-upload"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@@ -282,14 +299,14 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
|
||||
size="lg"
|
||||
className="text-neutral-600 dark:text-neutral-300"
|
||||
/>
|
||||
<span className="mt-1 font-sans text-xs font-normal text-neutral-600 dark:text-neutral-300">
|
||||
<span className="mt-1 font-sans text-sm font-normal text-neutral-600 dark:text-neutral-300">
|
||||
Add image
|
||||
</span>
|
||||
</label>
|
||||
</Label>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-2.5">
|
||||
{images.map((src, index) => (
|
||||
<div key={index} className="relative flex-shrink-0">
|
||||
<Image
|
||||
@@ -328,21 +345,21 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium leading-tight text-slate-950 dark:text-slate-300">
|
||||
<Label className="font-sans text-sm font-medium text-[#020617]">
|
||||
AI image generator
|
||||
</label>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-base font-normal leading-normal text-slate-700 dark:text-slate-400">
|
||||
</Label>
|
||||
<div className="flex flex-col justify-between gap-2 md:flex-row md:items-center">
|
||||
<p className="font-sans text-sm text-neutral-500 md:text-base">
|
||||
You can use AI to generate a cover image for you
|
||||
</p>
|
||||
<Button
|
||||
className={`bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-600 dark:hover:bg-neutral-500 ${
|
||||
className={`w-fit bg-neutral-800 font-sans text-white hover:bg-neutral-900 dark:bg-neutral-600 dark:hover:bg-neutral-500 ${
|
||||
images.length >= 5 ? "cursor-not-allowed opacity-50" : ""
|
||||
}`}
|
||||
onClick={handleGenerateImage}
|
||||
@@ -358,79 +375,78 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label
|
||||
<Label
|
||||
htmlFor="youtube"
|
||||
className="text-sm font-medium leading-tight text-slate-950 dark:text-slate-300"
|
||||
className="font-sans text-sm font-medium text-[#020617]"
|
||||
>
|
||||
YouTube video link
|
||||
</label>
|
||||
<input
|
||||
</Label>
|
||||
<Input
|
||||
id="youtube"
|
||||
type="text"
|
||||
placeholder="Paste a video link here"
|
||||
value={youtubeLink}
|
||||
onChange={(e) => setYoutubeLink(e.target.value)}
|
||||
className="w-full rounded-[55px] border border-slate-200 py-2.5 pl-4 pr-14 font-sans text-base font-normal leading-normal text-slate-500 dark:border-slate-700 dark:bg-gray-700 dark:text-slate-300"
|
||||
className="h-11 rounded-full border border-[#E2E8F0] px-4 py-2.5 font-sans text-sm text-neutral-500 md:text-base"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label
|
||||
<Label
|
||||
htmlFor="category"
|
||||
className="text-sm font-medium leading-tight text-slate-950 dark:text-slate-300"
|
||||
className="font-sans text-sm font-medium text-[#020617]"
|
||||
>
|
||||
Category
|
||||
</label>
|
||||
<select
|
||||
id="category"
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value)}
|
||||
className="w-full appearance-none rounded-[55px] border border-slate-200 py-2.5 pl-4 pr-5 font-sans text-base font-normal leading-normal text-slate-500 dark:border-slate-700 dark:bg-gray-700 dark:text-slate-300"
|
||||
>
|
||||
<option value="">Select a category for your agent</option>
|
||||
<option value="productivity">Productivity</option>
|
||||
<option value="writing">Writing & Content</option>
|
||||
<option value="development">Development</option>
|
||||
<option value="data">Data & Analytics</option>
|
||||
<option value="marketing">Marketing & SEO</option>
|
||||
<option value="research">Research & Learning</option>
|
||||
<option value="creative">Creative & Design</option>
|
||||
<option value="business">Business & Finance</option>
|
||||
<option value="personal">Personal Assistant</option>
|
||||
<option value="other">Other</option>
|
||||
{/* Add more options here */}
|
||||
</select>
|
||||
</Label>
|
||||
<Select value={category} onValueChange={setCategory}>
|
||||
<SelectTrigger className="h-11 rounded-full border border-[#E2E8F0] px-4 py-2.5 font-sans text-sm text-neutral-500 md:text-base">
|
||||
<SelectValue placeholder="Select a category for your agent" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="font-sans">
|
||||
<SelectItem value="productivity">Productivity</SelectItem>
|
||||
<SelectItem value="writing">Writing & Content</SelectItem>
|
||||
<SelectItem value="development">Development</SelectItem>
|
||||
<SelectItem value="data">Data & Analytics</SelectItem>
|
||||
<SelectItem value="marketing">Marketing & SEO</SelectItem>
|
||||
<SelectItem value="research">Research & Learning</SelectItem>
|
||||
<SelectItem value="creative">Creative & Design</SelectItem>
|
||||
<SelectItem value="business">Business & Finance</SelectItem>
|
||||
<SelectItem value="personal">Personal Assistant</SelectItem>
|
||||
<SelectItem value="other">Other</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label
|
||||
<Label
|
||||
htmlFor="description"
|
||||
className="text-sm font-medium leading-tight text-slate-950 dark:text-slate-300"
|
||||
className="font-sans text-sm font-medium text-[#020617]"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="Describe your agent and what it does"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
className="h-[100px] w-full resize-none rounded-2xl border border-slate-200 bg-white py-2.5 pl-4 pr-14 font-sans text-base font-normal leading-normal text-slate-900 dark:border-slate-700 dark:bg-gray-700 dark:text-slate-300"
|
||||
></textarea>
|
||||
className="h-[100px] w-full resize-none rounded-2xl border border-[#E2E8F0] px-4 py-2.5 font-sans text-sm text-neutral-500 md:text-base"
|
||||
></Textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom buttons */}
|
||||
<div className="flex justify-between gap-4 border-t border-slate-200 p-6 dark:border-slate-700">
|
||||
<Button
|
||||
onClick={onBack}
|
||||
size="lg"
|
||||
className="w-full dark:border-slate-700 dark:text-slate-300 sm:flex-1"
|
||||
className="flex w-full items-center justify-center text-sm dark:border-slate-700 dark:text-slate-300 sm:flex-1 md:text-base"
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
size="lg"
|
||||
className="w-full bg-neutral-800 text-white hover:bg-neutral-900 dark:bg-neutral-600 dark:hover:bg-neutral-500 sm:flex-1"
|
||||
className="flex w-full items-center justify-center bg-neutral-800 text-sm text-white hover:bg-neutral-900 dark:bg-neutral-600 dark:hover:bg-neutral-500 sm:flex-1 md:text-base"
|
||||
>
|
||||
Submit for review
|
||||
</Button>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { RatingCard } from "./RatingCard";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/RatingCard",
|
||||
component: RatingCard,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
} satisfies Meta<typeof RatingCard>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
agentName: "Test Agent",
|
||||
// onSubmit: (rating) => {
|
||||
// console.log("Rating submitted:", rating);
|
||||
// },
|
||||
// onClose: () => {
|
||||
// console.log("Rating card closed");
|
||||
// },
|
||||
storeListingVersionId: "1",
|
||||
},
|
||||
};
|
||||
|
||||
export const LongAgentName: Story = {
|
||||
args: {
|
||||
agentName: "Very Long Agent Name That Might Need Special Handling",
|
||||
// onSubmit: (rating) => {
|
||||
// console.log("Rating submitted:", rating);
|
||||
// },
|
||||
// onClose: () => {
|
||||
// console.log("Rating card closed");
|
||||
// },
|
||||
storeListingVersionId: "1",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutCallbacks: Story = {
|
||||
args: {
|
||||
agentName: "Test Agent",
|
||||
storeListingVersionId: "1",
|
||||
},
|
||||
};
|
||||
@@ -3,30 +3,13 @@ import { SearchBar } from "./SearchBar";
|
||||
import { userEvent, within, expect } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Search Bar",
|
||||
title: "Agpt UI/marketing/Search Bar",
|
||||
component: SearchBar,
|
||||
parameters: {
|
||||
layout: {
|
||||
center: true,
|
||||
padding: 0,
|
||||
},
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
navigation: {
|
||||
pathname: "/search",
|
||||
query: {
|
||||
searchTerm: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
placeholder: { control: "text" },
|
||||
backgroundColor: { control: "text" },
|
||||
iconColor: { control: "text" },
|
||||
textColor: { control: "text" },
|
||||
placeholderColor: { control: "text" },
|
||||
className: { control: "text" },
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
@@ -49,38 +32,25 @@ export const Default: Story = {
|
||||
export const CustomStyles: Story = {
|
||||
args: {
|
||||
placeholder: "Enter your search query",
|
||||
backgroundColor: "bg-blue-100",
|
||||
iconColor: "text-blue-500",
|
||||
textColor: "text-blue-700",
|
||||
placeholderColor: "text-blue-400",
|
||||
className: "bg-blue-100",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInteraction: Story = {
|
||||
export const TestingInteractions: Story = {
|
||||
args: {
|
||||
placeholder: "Type and press Enter",
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const input = canvas.getByPlaceholderText("Type and press Enter");
|
||||
|
||||
await userEvent.type(input, "test query");
|
||||
await userEvent.keyboard("{Enter}");
|
||||
|
||||
await expect(input).toHaveValue("test query");
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptySubmit: Story = {
|
||||
args: {
|
||||
placeholder: "Empty submit test",
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const input = canvas.getByPlaceholderText("Empty submit test");
|
||||
|
||||
await userEvent.keyboard("{Enter}");
|
||||
|
||||
await expect(input).toHaveValue("");
|
||||
// checking onChange in input
|
||||
const Input = canvas.getByTestId("store-search-input");
|
||||
await userEvent.type(Input, "test query", {
|
||||
delay: 100,
|
||||
});
|
||||
await userEvent.keyboard("{Enter}", {
|
||||
delay: 100,
|
||||
});
|
||||
await expect(Input).toHaveValue("test query");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,26 +4,18 @@ import * as React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
||||
import { Input } from "../ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SearchBarProps {
|
||||
placeholder?: string;
|
||||
backgroundColor?: string;
|
||||
iconColor?: string;
|
||||
textColor?: string;
|
||||
placeholderColor?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** SearchBar component for user input and search functionality. */
|
||||
export const SearchBar: React.FC<SearchBarProps> = ({
|
||||
placeholder = 'Search for tasks like "optimise SEO"',
|
||||
backgroundColor = "bg-neutral-100 dark:bg-neutral-800",
|
||||
iconColor = "text-[#646464] dark:text-neutral-400",
|
||||
textColor = "text-[#707070] dark:text-neutral-200",
|
||||
placeholderColor = "text-[#707070] dark:text-neutral-400",
|
||||
width = "w-9/10 lg:w-[56.25rem]",
|
||||
height = "h-[60px]",
|
||||
className,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -31,7 +23,6 @@ export const SearchBar: React.FC<SearchBarProps> = ({
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
console.log(searchQuery);
|
||||
|
||||
if (searchQuery.trim()) {
|
||||
// Encode the search term and navigate to the desired path
|
||||
@@ -44,15 +35,18 @@ export const SearchBar: React.FC<SearchBarProps> = ({
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
data-testid="store-search-bar"
|
||||
className={`${width} ${height} px-4 pt-2 md:px-6 md:pt-1 ${backgroundColor} flex items-center justify-center gap-2 rounded-full md:gap-5`}
|
||||
className={cn(
|
||||
`flex h-14 w-full items-center justify-center gap-2 rounded-full bg-[#F3F3F3] px-6 py-2.5 md:h-18 md:gap-5`,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<MagnifyingGlassIcon className={`h-5 w-5 md:h-7 md:w-7 ${iconColor}`} />
|
||||
<input
|
||||
<MagnifyingGlassIcon className={`h-5 w-5 text-[#020617] md:h-7 md:w-7`} />
|
||||
<Input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className={`flex-grow border-none bg-transparent ${textColor} font-sans text-lg font-normal leading-[2.25rem] tracking-tight md:text-xl placeholder:${placeholderColor} focus:outline-none`}
|
||||
className={`m-0 flex-grow border-none bg-transparent p-0 font-sans text-base font-normal text-zinc-800 shadow-none placeholder:text-neutral-500 focus:shadow-none focus:outline-none focus:ring-0 md:text-xl`}
|
||||
data-testid="store-search-input"
|
||||
/>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { SearchFilterChips } from "./SearchFilterChips";
|
||||
|
||||
const meta = {
|
||||
title: "Agpt UI/marketing/Search Filter Chips",
|
||||
component: SearchFilterChips,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
onFilterChange: { action: "onFilterChange" },
|
||||
},
|
||||
} satisfies Meta<typeof SearchFilterChips>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import AutogptButton from "./AutogptButton";
|
||||
|
||||
interface FilterOption {
|
||||
label: string;
|
||||
@@ -38,26 +40,22 @@ export const SearchFilterChips: React.FC<SearchFilterChipsProps> = ({
|
||||
return (
|
||||
<div className="flex gap-2.5">
|
||||
{filters.map((filter) => (
|
||||
<button
|
||||
<AutogptButton
|
||||
key={filter.value}
|
||||
variant={selected === filter.value ? "default" : "outline"}
|
||||
onClick={() => handleFilterClick(filter.value)}
|
||||
className={`flex items-center gap-2.5 rounded-[34px] px-5 py-2 ${
|
||||
selected === filter.value
|
||||
? "bg-neutral-800 text-white dark:bg-neutral-100 dark:text-neutral-900"
|
||||
: "border border-neutral-600 text-neutral-800 dark:border-neutral-400 dark:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`text-base ${selected === filter.value ? "font-medium" : ""}`}
|
||||
className={`mr-2 font-sans text-base ${selected === filter.value ? "font-medium" : "font-normal"}`}
|
||||
>
|
||||
{filter.label}
|
||||
</span>
|
||||
<span
|
||||
className={`text-base ${selected === filter.value ? "font-medium" : ""}`}
|
||||
className={`font-sans text-base ${selected === filter.value ? "font-medium" : "font-normal"}`}
|
||||
>
|
||||
{filter.count}
|
||||
</span>
|
||||
</button>
|
||||
</AutogptButton>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Sidebar",
|
||||
title: "Agpt UI/marketing/Sidebar",
|
||||
component: Sidebar,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"use client";
|
||||
import * as React from "react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "./Button";
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
||||
import { Menu } from "lucide-react";
|
||||
import { IconDashboardLayout } from "../ui/icons";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export interface SidebarLink {
|
||||
text: string;
|
||||
@@ -28,54 +27,32 @@ const getDefaultIconForLink = () => {
|
||||
export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
|
||||
// Extract all links from linkGroups
|
||||
const allLinks = linkGroups.flatMap((group) => group.links);
|
||||
const pathname = usePathname();
|
||||
|
||||
// Function to render link items
|
||||
const renderLinks = () => {
|
||||
return allLinks.map((link, index) => (
|
||||
<Link
|
||||
key={`${link.href}-${index}`}
|
||||
href={link.href}
|
||||
className="inline-flex w-full items-center gap-2.5 rounded-xl px-3 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
|
||||
>
|
||||
{link.icon || getDefaultIconForLink()}
|
||||
<div className="p-ui-medium text-base font-medium leading-normal">
|
||||
{link.text}
|
||||
</div>
|
||||
</Link>
|
||||
));
|
||||
return allLinks.map((link, index) => {
|
||||
const isActive = pathname === link.href;
|
||||
return (
|
||||
<Link
|
||||
key={`${link.href}-${index}`}
|
||||
href={link.href}
|
||||
className={`inline-flex w-full items-center gap-2.5 rounded-xl p-3 ${
|
||||
isActive
|
||||
? "bg-zinc-800 text-white dark:bg-neutral-700 dark:text-white"
|
||||
: "text-neutral-800 hover:bg-zinc-800 hover:text-white dark:text-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{link.icon || getDefaultIconForLink()}
|
||||
<p className="font-sans text-base font-medium">{link.text}</p>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
aria-label="Open sidebar menu"
|
||||
className="fixed left-4 top-4 z-50 flex h-14 w-14 items-center justify-center rounded-lg border border-neutral-500 bg-neutral-200 hover:bg-gray-200/50 dark:border-neutral-700 dark:bg-neutral-800 dark:hover:bg-gray-700/50 md:block lg:hidden"
|
||||
>
|
||||
<Menu className="h-8 w-8 stroke-black dark:stroke-white" />
|
||||
<span className="sr-only">Open sidebar menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent
|
||||
side="left"
|
||||
className="z-50 w-[280px] border-none p-0 dark:bg-neutral-900 sm:w-[280px]"
|
||||
>
|
||||
<div className="h-full w-full rounded-2xl bg-zinc-200 dark:bg-zinc-800">
|
||||
<div className="inline-flex h-[264px] flex-col items-start justify-start gap-6 p-3">
|
||||
{renderLinks()}
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<div className="relative hidden h-[912px] w-[234px] border-none lg:block">
|
||||
<div className="h-full w-full rounded-2xl bg-zinc-200 dark:bg-zinc-800">
|
||||
<div className="inline-flex h-[264px] flex-col items-start justify-start gap-6 p-3">
|
||||
{renderLinks()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<div className="sticky top-24 flex h-[calc(100vh-7rem)] w-60 flex-col gap-6 rounded-[1rem] bg-zinc-200 p-3">
|
||||
{renderLinks()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -34,22 +34,22 @@ export const SortDropdown: React.FC<{
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex items-center gap-1.5 focus:outline-none">
|
||||
<span className="font-geist text-base text-neutral-800 dark:text-neutral-200">
|
||||
<span className="font-sans text-sm font-medium text-zinc-800 dark:text-neutral-200">
|
||||
Sort by
|
||||
</span>
|
||||
<span className="font-geist text-base text-neutral-800 dark:text-neutral-200">
|
||||
<span className="font-sans text-sm font-medium text-zinc-800 dark:text-neutral-200">
|
||||
{selected.label}
|
||||
</span>
|
||||
<ChevronDownIcon className="h-4 w-4 text-neutral-800 dark:text-neutral-200" />
|
||||
<ChevronDownIcon className="h-4 w-4 text-zinc-800 dark:text-neutral-200" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-[200px] rounded-lg bg-white shadow-lg dark:bg-neutral-800"
|
||||
className="w-52 rounded-lg bg-white shadow-lg dark:bg-neutral-800"
|
||||
>
|
||||
{sortOptions.map((option) => (
|
||||
<DropdownMenuItem
|
||||
key={option.value}
|
||||
className={`cursor-pointer px-4 py-2 text-base hover:bg-neutral-100 dark:hover:bg-neutral-700 ${
|
||||
className={`cursor-pointer px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-neutral-700 ${
|
||||
selected.value === option.value
|
||||
? "font-medium text-neutral-800 dark:text-neutral-200"
|
||||
: "text-neutral-600 dark:text-neutral-400"
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Status, StatusType } from "./Status";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Status",
|
||||
title: "Agpt UI/general/Status",
|
||||
component: Status,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
@@ -42,17 +42,3 @@ export const Rejected: Story = {
|
||||
status: "rejected" as StatusType,
|
||||
},
|
||||
};
|
||||
|
||||
export const AllStatuses: Story = {
|
||||
args: {
|
||||
status: "draft" as StatusType,
|
||||
},
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Status status="draft" />
|
||||
<Status status="awaiting_review" />
|
||||
<Status status="approved" />
|
||||
<Status status="rejected" />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -12,46 +12,31 @@ const statusConfig: Record<
|
||||
bgColor: string;
|
||||
dotColor: string;
|
||||
text: string;
|
||||
darkBgColor: string;
|
||||
darkDotColor: string;
|
||||
}
|
||||
> = {
|
||||
draft: {
|
||||
bgColor: "bg-blue-50",
|
||||
dotColor: "bg-blue-500",
|
||||
text: "Draft",
|
||||
darkBgColor: "dark:bg-blue-900",
|
||||
darkDotColor: "dark:bg-blue-300",
|
||||
},
|
||||
awaiting_review: {
|
||||
bgColor: "bg-amber-50",
|
||||
dotColor: "bg-amber-500",
|
||||
text: "Awaiting review",
|
||||
darkBgColor: "dark:bg-amber-900",
|
||||
darkDotColor: "dark:bg-amber-300",
|
||||
},
|
||||
approved: {
|
||||
bgColor: "bg-green-50",
|
||||
dotColor: "bg-green-500",
|
||||
text: "Approved",
|
||||
darkBgColor: "dark:bg-green-900",
|
||||
darkDotColor: "dark:bg-green-300",
|
||||
},
|
||||
rejected: {
|
||||
bgColor: "bg-red-50",
|
||||
dotColor: "bg-red-500",
|
||||
text: "Rejected",
|
||||
darkBgColor: "dark:bg-red-900",
|
||||
darkDotColor: "dark:bg-red-300",
|
||||
},
|
||||
};
|
||||
|
||||
export const Status: React.FC<StatusProps> = ({ status }) => {
|
||||
/**
|
||||
* Status component displays a badge with a colored dot and text indicating the agent's status
|
||||
* @param status - The current status of the agent
|
||||
* Valid values: 'draft', 'awaiting_review', 'approved', 'rejected'
|
||||
*/
|
||||
if (!status) {
|
||||
return <Status status="awaiting_review" />;
|
||||
} else if (!statusConfig[status]) {
|
||||
@@ -62,12 +47,10 @@ export const Status: React.FC<StatusProps> = ({ status }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`px-2.5 py-1 ${config.bgColor} ${config.darkBgColor} flex items-center gap-1.5 rounded-[26px]`}
|
||||
className={`px-2.5 py-1 ${config.bgColor} flex w-fit items-center gap-1.5 rounded-3xl`}
|
||||
>
|
||||
<div
|
||||
className={`h-3 w-3 ${config.dotColor} ${config.darkDotColor} rounded-full`}
|
||||
/>
|
||||
<div className="font-sans text-sm font-normal leading-tight text-neutral-600 dark:text-neutral-300">
|
||||
<div className={`h-3 w-3 ${config.dotColor} rounded-full`} />
|
||||
<div className="font-sans text-sm font-normal text-neutral-600">
|
||||
{config.text}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { StoreCard } from "./StoreCard";
|
||||
import { userEvent, within, expect } from "@storybook/test";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/StoreCard",
|
||||
title: "Agpt UI/marketing/StoreCard",
|
||||
component: StoreCard,
|
||||
parameters: {
|
||||
layout: {
|
||||
center: true,
|
||||
fullscreen: true,
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
agentName: { control: "text" },
|
||||
@@ -21,6 +22,8 @@ const meta = {
|
||||
rating: { control: "number", min: 0, max: 5, step: 0.1 },
|
||||
onClick: { action: "clicked" },
|
||||
avatarSrc: { control: "text" },
|
||||
hideAvatar: { control: "boolean" },
|
||||
creatorName: { control: "text" },
|
||||
},
|
||||
} satisfies Meta<typeof StoreCard>;
|
||||
|
||||
@@ -30,86 +33,64 @@ type Story = StoryObj<typeof meta>;
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
agentName: "SEO Optimizer",
|
||||
agentImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
description: "Optimize your website's SEO with AI-powered suggestions",
|
||||
agentImage: "/testing_agent_image.jpg",
|
||||
description:
|
||||
"Optimize your website's SEO with AI-powered suggestions and best practices. Get detailed reports and actionable recommendations.",
|
||||
runs: 10000,
|
||||
rating: 4.5,
|
||||
onClick: () => console.log("Default StoreCard clicked"),
|
||||
avatarSrc: "https://github.com/shadcn.png",
|
||||
},
|
||||
};
|
||||
|
||||
export const LowRating: Story = {
|
||||
args: {
|
||||
agentName: "Data Analyzer",
|
||||
agentImage:
|
||||
"https://upload.wikimedia.org/wikipedia/commons/c/c5/Big_buck_bunny_poster_big.jpg",
|
||||
description: "Analyze complex datasets with machine learning algorithms",
|
||||
runs: 5000,
|
||||
rating: 2.7,
|
||||
onClick: () => console.log("LowRating StoreCard clicked"),
|
||||
avatarSrc: "https://example.com/avatar2.jpg",
|
||||
},
|
||||
};
|
||||
|
||||
export const HighRuns: Story = {
|
||||
args: {
|
||||
agentName: "Code Assistant",
|
||||
agentImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
description: "Get AI-powered coding help for various programming languages",
|
||||
runs: 1000000,
|
||||
rating: 4.8,
|
||||
onClick: () => console.log("HighRuns StoreCard clicked"),
|
||||
avatarSrc: "https://example.com/avatar3.jpg",
|
||||
avatarSrc: "/testing_avatar.png",
|
||||
creatorName: "AI Solutions Inc.",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInteraction: Story = {
|
||||
args: {
|
||||
agentName: "Task Planner",
|
||||
agentImage:
|
||||
"https://upload.wikimedia.org/wikipedia/commons/c/c5/Big_buck_bunny_poster_big.jpg",
|
||||
description: "Plan and organize your tasks efficiently with AI",
|
||||
agentImage: Default.args.agentImage,
|
||||
description:
|
||||
"Plan and organize your tasks efficiently with AI assistance. Set priorities, deadlines, and track progress.",
|
||||
runs: 50000,
|
||||
rating: 4.2,
|
||||
onClick: () => console.log("WithInteraction StoreCard clicked"),
|
||||
avatarSrc: "https://example.com/avatar4.jpg",
|
||||
avatarSrc: Default.args.avatarSrc,
|
||||
creatorName: "Productivity Plus",
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const storeCard = canvas.getByText("Task Planner");
|
||||
const storeCard = canvas.getByTestId("store-card");
|
||||
|
||||
await userEvent.hover(storeCard);
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
await userEvent.click(storeCard);
|
||||
},
|
||||
};
|
||||
|
||||
export const LongDescription: Story = {
|
||||
export const LongContent: Story = {
|
||||
args: {
|
||||
agentName: "AI Writing Assistant",
|
||||
agentImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
agentName:
|
||||
"AI Writing Assistant that can help you to write some long content so you do not have to hire for writing some basic arrangement of letters",
|
||||
agentImage: Default.args.agentImage,
|
||||
description:
|
||||
"Enhance your writing with our advanced AI-powered assistant. It offers real-time suggestions for grammar, style, and tone, helps with research and fact-checking.",
|
||||
"Enhance your writing with our advanced AI-powered assistant. It offers real-time suggestions for grammar, style, and tone, helps with research and fact-checking, and provides vocabulary enhancements for more engaging content. Perfect for content creators, marketers, and writers of all levels.",
|
||||
runs: 75000,
|
||||
rating: 4.7,
|
||||
onClick: () => console.log("LongDescription StoreCard clicked"),
|
||||
avatarSrc: "https://example.com/avatar5.jpg",
|
||||
onClick: () => console.log("LongContent StoreCard clicked"),
|
||||
avatarSrc: Default.args.avatarSrc,
|
||||
creatorName:
|
||||
"The person who created the multiverst, including earth no. 631 and more..",
|
||||
},
|
||||
};
|
||||
|
||||
export const HiddenAvatar: Story = {
|
||||
export const SmallContent: Story = {
|
||||
args: {
|
||||
agentName: "Data Visualizer",
|
||||
agentImage:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
description: "Create stunning visualizations from complex datasets",
|
||||
runs: 60000,
|
||||
rating: 4.6,
|
||||
onClick: () => console.log("HiddenAvatar StoreCard clicked"),
|
||||
avatarSrc: "https://example.com/avatar6.jpg",
|
||||
hideAvatar: true,
|
||||
agentName: "Quick Notes",
|
||||
agentImage: Default.args.agentImage,
|
||||
description: "Simple note-taking assistant.",
|
||||
runs: 3000,
|
||||
rating: 4.0,
|
||||
onClick: () => console.log("SmallContent StoreCard clicked"),
|
||||
avatarSrc: Default.args.avatarSrc,
|
||||
creatorName: "Note Systems",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ export const StoreCard: React.FC<StoreCardProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-[27rem] w-full max-w-md cursor-pointer flex-col items-start rounded-3xl bg-white transition-all duration-300 hover:shadow-lg dark:bg-transparent dark:hover:shadow-gray-700"
|
||||
className="w-full min-w-80 max-w-md space-y-2 rounded-3xl bg-white p-2 pb-3 hover:bg-gray-50"
|
||||
onClick={handleClick}
|
||||
data-testid="store-card"
|
||||
role="button"
|
||||
@@ -45,7 +45,7 @@ export const StoreCard: React.FC<StoreCardProps> = ({
|
||||
}}
|
||||
>
|
||||
{/* First Section: Image with Avatar */}
|
||||
<div className="relative aspect-[2/1.2] w-full overflow-hidden rounded-3xl md:aspect-[2.17/1]">
|
||||
<div className="relative aspect-[2/1.2] w-full overflow-hidden rounded-3xl md:aspect-[1.78/1]">
|
||||
{agentImage && (
|
||||
<Image
|
||||
src={agentImage}
|
||||
@@ -56,7 +56,7 @@ export const StoreCard: React.FC<StoreCardProps> = ({
|
||||
/>
|
||||
)}
|
||||
{!hideAvatar && (
|
||||
<div className="absolute bottom-4 left-4">
|
||||
<div className="absolute bottom-4 left-4 rounded-full border border-zinc-200">
|
||||
<Avatar className="h-16 w-16">
|
||||
{avatarSrc && (
|
||||
<AvatarImage
|
||||
@@ -72,46 +72,40 @@ export const StoreCard: React.FC<StoreCardProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex w-full flex-1 flex-col px-4">
|
||||
<div className="flex w-full flex-1 flex-col">
|
||||
{/* Second Section: Agent Name and Creator Name */}
|
||||
<div className="flex w-full flex-col">
|
||||
<h3 className="line-clamp-2 font-poppins text-2xl font-semibold text-[#272727] dark:text-neutral-100">
|
||||
<div className="flex w-full flex-col px-1.5">
|
||||
<h3 className="line-clamp-2 h-12 font-sans text-base font-medium text-zinc-800 dark:text-neutral-100">
|
||||
{agentName}
|
||||
</h3>
|
||||
{!hideAvatar && creatorName && (
|
||||
<p className="mt-3 truncate font-sans text-xl font-normal text-neutral-600 dark:text-neutral-400">
|
||||
<p className="truncate font-sans text-sm font-normal text-zinc-600 dark:text-neutral-400">
|
||||
by {creatorName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Third Section: Description */}
|
||||
<div className="mt-2.5 flex w-full flex-col">
|
||||
<p className="line-clamp-3 font-sans text-base font-normal leading-normal text-neutral-600 dark:text-neutral-400">
|
||||
<div className="flex h-18 w-full flex-col px-1.5 pt-2">
|
||||
<p className="line-clamp-3 font-sans text-sm font-normal text-zinc-500 dark:text-neutral-400">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow" />
|
||||
{/* Spacer to push stats to bottom */}
|
||||
|
||||
{/* Fourth Section: Stats Row - aligned to bottom */}
|
||||
<div className="mt-5 w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="font-sans text-lg font-semibold text-neutral-800 dark:text-neutral-200">
|
||||
{runs.toLocaleString()} runs
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-sans text-lg font-semibold text-neutral-800 dark:text-neutral-200">
|
||||
{rating.toFixed(1)}
|
||||
</span>
|
||||
<div
|
||||
className="inline-flex items-center"
|
||||
role="img"
|
||||
aria-label={`Rating: ${rating.toFixed(1)} out of 5 stars`}
|
||||
>
|
||||
{StarRatingIcons(rating)}
|
||||
</div>
|
||||
<div className="mt-2.5 flex items-center justify-between px-1.5 pt-2">
|
||||
<div className="font-sans text-sm font-medium text-zinc-800 dark:text-neutral-200">
|
||||
{runs.toLocaleString()} runs
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-sans text-sm font-medium text-zinc-800 dark:text-neutral-200">
|
||||
{rating.toFixed(1)}
|
||||
</span>
|
||||
<div
|
||||
className="inline-flex items-center"
|
||||
role="img"
|
||||
aria-label={`Rating: ${rating.toFixed(1)} out of 5 stars`}
|
||||
>
|
||||
{StarRatingIcons(rating)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,63 +1,65 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Agent, AgentsSection } from "./AgentsSection";
|
||||
import { userEvent, within, expect } from "@storybook/test";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Composite/Agents Section",
|
||||
title: "Agpt UI/marketing/Agents Section",
|
||||
component: AgentsSection,
|
||||
parameters: {
|
||||
layout: {
|
||||
center: true,
|
||||
fullscreen: true,
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex items-center justify-center py-4 md:p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
sectionTitle: { control: "text" },
|
||||
agents: { control: "object" },
|
||||
// onCardClick: { action: "clicked" },
|
||||
hideAvatars: { control: "boolean" },
|
||||
margin: { control: "text" },
|
||||
className: { control: "text" },
|
||||
},
|
||||
} satisfies Meta<typeof AgentsSection>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const defaultAgentImage = "/testing_agent_image.jpg";
|
||||
const defaultAvatarImage = "/testing_avatar.png";
|
||||
const mockTopAgents = [
|
||||
{
|
||||
agent_name: "SEO Optimizer Pro",
|
||||
agent_image:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
agent_image: defaultAgentImage,
|
||||
description:
|
||||
"Boost your website's search engine rankings with our advanced AI-powered SEO optimization tool.",
|
||||
runs: 50000,
|
||||
rating: 4.7,
|
||||
creator_avatar: "https://example.com/avatar1.jpg",
|
||||
creator_avatar: defaultAvatarImage,
|
||||
slug: "seo-optimizer-pro",
|
||||
creator: "John Doe",
|
||||
sub_heading: "SEO Expert",
|
||||
},
|
||||
{
|
||||
agent_name: "Content Writer AI",
|
||||
agent_image:
|
||||
"https://upload.wikimedia.org/wikipedia/commons/c/c5/Big_buck_bunny_poster_big.jpg",
|
||||
agent_image: defaultAgentImage,
|
||||
description:
|
||||
"Generate high-quality, engaging content for your blog, social media, or marketing campaigns.",
|
||||
runs: 75000,
|
||||
rating: 4.5,
|
||||
creator_avatar: "https://example.com/avatar2.jpg",
|
||||
creator_avatar: defaultAvatarImage,
|
||||
slug: "content-writer-ai",
|
||||
creator: "Jane Doe",
|
||||
sub_heading: "Content Writer",
|
||||
},
|
||||
{
|
||||
agent_name: "Data Analyzer Lite",
|
||||
agent_image:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
agent_image: defaultAgentImage,
|
||||
description: "A basic tool for analyzing small to medium-sized datasets.",
|
||||
runs: 10000,
|
||||
rating: 3.8,
|
||||
creator_avatar: "https://example.com/avatar3.jpg",
|
||||
creator_avatar: defaultAvatarImage,
|
||||
slug: "data-analyzer-lite",
|
||||
creator: "John Doe",
|
||||
sub_heading: "Data Analyst",
|
||||
@@ -68,132 +70,115 @@ export const Default: Story = {
|
||||
args: {
|
||||
sectionTitle: "Top Agents",
|
||||
agents: mockTopAgents,
|
||||
// onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
|
||||
},
|
||||
};
|
||||
|
||||
export const SingleAgent: Story = {
|
||||
args: {
|
||||
sectionTitle: "Top Agents",
|
||||
sectionTitle: "Featured Agent",
|
||||
agents: [mockTopAgents[0]],
|
||||
// onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
|
||||
},
|
||||
};
|
||||
|
||||
export const NoAgents: Story = {
|
||||
args: {
|
||||
sectionTitle: "Top Agents",
|
||||
sectionTitle: "Recommended Agents",
|
||||
agents: [],
|
||||
// onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInteraction: Story = {
|
||||
args: {
|
||||
sectionTitle: "Top Agents",
|
||||
agents: mockTopAgents,
|
||||
// onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const firstCard = canvas.getAllByRole("store-card")[0];
|
||||
await userEvent.click(firstCard);
|
||||
await expect(firstCard).toHaveAttribute("aria-pressed", "true");
|
||||
},
|
||||
};
|
||||
|
||||
export const MultiRowAgents: Story = {
|
||||
args: {
|
||||
sectionTitle: "Top Agents",
|
||||
sectionTitle: "All Agents",
|
||||
agents: [
|
||||
...mockTopAgents,
|
||||
{
|
||||
agent_name: "Image Recognition AI",
|
||||
agent_image:
|
||||
"https://upload.wikimedia.org/wikipedia/commons/c/c5/Big_buck_bunny_poster_big.jpg",
|
||||
agent_image: defaultAgentImage,
|
||||
description:
|
||||
"Accurately identify and classify objects in images using state-of-the-art machine learning algorithms.",
|
||||
runs: 60000,
|
||||
rating: 4.6,
|
||||
creator_avatar: "https://example.com/avatar4.jpg",
|
||||
creator_avatar: defaultAvatarImage,
|
||||
slug: "image-recognition-ai",
|
||||
creator: "John Doe",
|
||||
creator: "Alex Smith",
|
||||
sub_heading: "Image Recognition",
|
||||
},
|
||||
{
|
||||
agent_name: "Natural Language Processor",
|
||||
agent_image:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
agent_image: defaultAgentImage,
|
||||
description:
|
||||
"Analyze and understand human language with advanced NLP techniques.",
|
||||
runs: 80000,
|
||||
rating: 4.8,
|
||||
creator_avatar: "https://example.com/avatar5.jpg",
|
||||
creator_avatar: defaultAvatarImage,
|
||||
slug: "natural-language-processor",
|
||||
creator: "John Doe",
|
||||
creator: "Maria Garcia",
|
||||
sub_heading: "Natural Language Processing",
|
||||
},
|
||||
{
|
||||
agent_name: "Sentiment Analyzer",
|
||||
agent_image:
|
||||
"https://upload.wikimedia.org/wikipedia/commons/c/c5/Big_buck_bunny_poster_big.jpg",
|
||||
agent_image: defaultAgentImage,
|
||||
description:
|
||||
"Determine the emotional tone of text data for customer feedback analysis.",
|
||||
runs: 45000,
|
||||
rating: 4.3,
|
||||
creator_avatar: "https://example.com/avatar6.jpg",
|
||||
creator_avatar: defaultAvatarImage,
|
||||
slug: "sentiment-analyzer",
|
||||
creator: "John Doe",
|
||||
creator: "Robert Johnson",
|
||||
sub_heading: "Sentiment Analysis",
|
||||
},
|
||||
{
|
||||
agent_name: "Chatbot Builder",
|
||||
agent_image:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
agent_image: defaultAgentImage,
|
||||
description:
|
||||
"Create intelligent chatbots for customer service and engagement.",
|
||||
runs: 55000,
|
||||
rating: 4.4,
|
||||
creator_avatar: "https://example.com/avatar7.jpg",
|
||||
creator_avatar: defaultAvatarImage,
|
||||
slug: "chatbot-builder",
|
||||
creator: "John Doe",
|
||||
creator: "Emma Wilson",
|
||||
sub_heading: "Chatbot Developer",
|
||||
},
|
||||
{
|
||||
agent_name: "Predictive Analytics Tool",
|
||||
agent_image:
|
||||
"https://upload.wikimedia.org/wikipedia/commons/c/c5/Big_buck_bunny_poster_big.jpg",
|
||||
agent_image: defaultAgentImage,
|
||||
description:
|
||||
"Forecast future trends and outcomes based on historical data.",
|
||||
runs: 40000,
|
||||
rating: 4.2,
|
||||
creator_avatar: "https://example.com/avatar8.jpg",
|
||||
creator_avatar: defaultAvatarImage,
|
||||
slug: "predictive-analytics-tool",
|
||||
creator: "John Doe",
|
||||
creator: "David Lee",
|
||||
sub_heading: "Predictive Analytics",
|
||||
},
|
||||
{
|
||||
agent_name: "Text-to-Speech Converter",
|
||||
agent_image:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
agent_image: defaultAgentImage,
|
||||
description:
|
||||
"Convert written text into natural-sounding speech in multiple languages.",
|
||||
runs: 35000,
|
||||
rating: 4.1,
|
||||
creator_avatar: "https://example.com/avatar9.jpg",
|
||||
creator_avatar: defaultAvatarImage,
|
||||
slug: "text-to-speech-converter",
|
||||
creator: "John Doe",
|
||||
creator: "Sarah Brown",
|
||||
sub_heading: "Text-to-Speech",
|
||||
},
|
||||
],
|
||||
// onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
|
||||
},
|
||||
};
|
||||
|
||||
export const HiddenAvatars: Story = {
|
||||
export const WithInteraction: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
hideAvatars: true,
|
||||
sectionTitle: "Agents with Hidden Avatars",
|
||||
sectionTitle: "Popular Agents",
|
||||
agents: mockTopAgents,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
// Using the proper test ID that's defined in the StoreCard component
|
||||
const firstCard = canvas.getAllByTestId("store-card")[0];
|
||||
await userEvent.hover(firstCard);
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
await userEvent.click(firstCard);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -26,13 +26,14 @@ interface AgentsSectionProps {
|
||||
agents: Agent[];
|
||||
hideAvatars?: boolean;
|
||||
margin?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const AgentsSection: React.FC<AgentsSectionProps> = ({
|
||||
sectionTitle,
|
||||
agents: allAgents,
|
||||
hideAvatars = false,
|
||||
margin = "37px",
|
||||
className,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -46,64 +47,32 @@ export const AgentsSection: React.FC<AgentsSectionProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className="w-full max-w-[1360px]">
|
||||
<div
|
||||
className={`mb-[${margin}] font-poppins text-lg font-semibold text-[#282828] dark:text-neutral-200`}
|
||||
>
|
||||
{sectionTitle}
|
||||
<div className={`w-full space-y-9 ${className}`}>
|
||||
<h2 className="font-poppins text-base font-medium text-zinc-500">
|
||||
{sectionTitle ? sectionTitle : "Top agents"}
|
||||
</h2>
|
||||
{!displayedAgents || displayedAgents.length === 0 ? (
|
||||
<div className="font-poppins text-gray-500 dark:text-gray-400">
|
||||
No agents found
|
||||
</div>
|
||||
{!displayedAgents || displayedAgents.length === 0 ? (
|
||||
<div className="text-center text-gray-500 dark:text-gray-400">
|
||||
No agents found
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Mobile Carousel View */}
|
||||
<Carousel
|
||||
className="md:hidden"
|
||||
opts={{
|
||||
loop: true,
|
||||
}}
|
||||
>
|
||||
<CarouselContent>
|
||||
{displayedAgents.map((agent, index) => (
|
||||
<CarouselItem key={index} className="min-w-64 max-w-71">
|
||||
<StoreCard
|
||||
agentName={agent.agent_name}
|
||||
agentImage={agent.agent_image}
|
||||
description={agent.description}
|
||||
runs={agent.runs}
|
||||
rating={agent.rating}
|
||||
avatarSrc={agent.creator_avatar}
|
||||
creatorName={agent.creator}
|
||||
hideAvatar={hideAvatars}
|
||||
onClick={() => handleCardClick(agent.creator, agent.slug)}
|
||||
/>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
</Carousel>
|
||||
|
||||
<div className="hidden grid-cols-1 place-items-center gap-6 md:grid md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
|
||||
{displayedAgents.map((agent, index) => (
|
||||
<StoreCard
|
||||
key={index}
|
||||
agentName={agent.agent_name}
|
||||
agentImage={agent.agent_image}
|
||||
description={agent.description}
|
||||
runs={agent.runs}
|
||||
rating={agent.rating}
|
||||
avatarSrc={agent.creator_avatar}
|
||||
creatorName={agent.creator}
|
||||
hideAvatar={hideAvatars}
|
||||
onClick={() => handleCardClick(agent.creator, agent.slug)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 place-items-center gap-5 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
|
||||
{displayedAgents.map((agent, index) => (
|
||||
<StoreCard
|
||||
key={index}
|
||||
agentName={agent.agent_name}
|
||||
agentImage={agent.agent_image}
|
||||
description={agent.description}
|
||||
runs={agent.runs}
|
||||
rating={agent.rating}
|
||||
avatarSrc={agent.creator_avatar}
|
||||
creatorName={agent.creator}
|
||||
hideAvatar={hideAvatars}
|
||||
onClick={() => handleCardClick(agent.creator, agent.slug)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { FeaturedCreators } from "./FeaturedCreators";
|
||||
import { userEvent, within, expect } from "@storybook/test";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Composite/Featured Creators",
|
||||
title: "Agpt UI/marketing/Featured Creators",
|
||||
component: FeaturedCreators,
|
||||
parameters: {
|
||||
layout: {
|
||||
center: true,
|
||||
fullscreen: true,
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
featuredCreators: { control: "object" },
|
||||
@@ -98,7 +98,6 @@ export const ManyCreators: Story = {
|
||||
num_agents: 25,
|
||||
},
|
||||
],
|
||||
// onCardClick: (creatorName) => console.log(`Clicked on ${creatorName}`),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -109,13 +108,10 @@ export const WithInteraction: Story = {
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const creatorCards = canvas.getAllByRole("creator-card");
|
||||
const creatorCards = canvas.getAllByTestId("creator-card");
|
||||
const firstCreatorCard = creatorCards[0];
|
||||
|
||||
await userEvent.hover(firstCreatorCard);
|
||||
await userEvent.click(firstCreatorCard);
|
||||
|
||||
// Check if the card has the expected hover and click effects
|
||||
await expect(firstCreatorCard).toHaveClass("hover:shadow-lg");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -31,25 +31,22 @@ export const FeaturedCreators: React.FC<FeaturedCreatorsProps> = ({
|
||||
const displayedCreators = featuredCreators.slice(0, 4);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center justify-center">
|
||||
<div className="w-full max-w-[1360px]">
|
||||
<h2 className="mb-9 font-poppins text-lg font-semibold text-neutral-800 dark:text-neutral-200">
|
||||
{title}
|
||||
</h2>
|
||||
<div className="w-full space-y-9">
|
||||
<h2 className="font-poppins text-base font-medium text-zinc-500 dark:text-zinc-200">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
{displayedCreators.map((creator, index) => (
|
||||
<CreatorCard
|
||||
key={index}
|
||||
creatorName={creator.name || creator.username}
|
||||
creatorImage={creator.avatar_url}
|
||||
bio={creator.description}
|
||||
agentsUploaded={creator.num_agents}
|
||||
onClick={() => handleCardClick(creator.username)}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-5">
|
||||
{displayedCreators.map((creator, index) => (
|
||||
<CreatorCard
|
||||
key={index}
|
||||
creatorName={creator.name || creator.username}
|
||||
creatorImage={creator.avatar_url}
|
||||
bio={creator.description}
|
||||
agentsUploaded={creator.num_agents}
|
||||
onClick={() => handleCardClick(creator.username)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { FeaturedSection } from "./FeaturedSection";
|
||||
import { userEvent, within } from "@storybook/test";
|
||||
import { userEvent, within, expect } from "@storybook/test";
|
||||
import { StoreAgent } from "@/lib/autogpt-server-api";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Composite/Featured Agents",
|
||||
title: "Agpt UI/marketing/Featured Section",
|
||||
component: FeaturedSection,
|
||||
parameters: {
|
||||
layout: {
|
||||
center: true,
|
||||
fullscreen: true,
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
featuredAgents: { control: "object" },
|
||||
@@ -32,10 +32,8 @@ const mockFeaturedAgents = [
|
||||
"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.",
|
||||
runs: 50000,
|
||||
rating: 4.7,
|
||||
agent_image:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
creator_avatar:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
agent_image: "/testing_agent_image.jpg",
|
||||
creator_avatar: "/testing_avatar.png",
|
||||
slug: "personalized-morning-coffee-newsletter",
|
||||
},
|
||||
{
|
||||
@@ -46,10 +44,8 @@ const mockFeaturedAgents = [
|
||||
"A lightweight data analysis tool for basic data processing needs.",
|
||||
runs: 10000,
|
||||
rating: 2.8,
|
||||
agent_image:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
creator_avatar:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
agent_image: "/testing_agent_image.jpg",
|
||||
creator_avatar: "/testing_avatar.png",
|
||||
slug: "data-analyzer-lite",
|
||||
},
|
||||
{
|
||||
@@ -60,10 +56,8 @@ const mockFeaturedAgents = [
|
||||
"An intelligent coding assistant that helps developers write better code faster.",
|
||||
runs: 1000000,
|
||||
rating: 4.9,
|
||||
agent_image:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
creator_avatar:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
agent_image: "/testing_agent_image.jpg",
|
||||
creator_avatar: "/testing_avatar.png",
|
||||
slug: "codeassist-ai",
|
||||
},
|
||||
{
|
||||
@@ -74,10 +68,8 @@ const mockFeaturedAgents = [
|
||||
"A comprehensive productivity suite that combines task management, note-taking, and project planning into one seamless interface. Features include smart task prioritization, automated scheduling, and AI-powered insights to help you work more efficiently.",
|
||||
runs: 75000,
|
||||
rating: 4.5,
|
||||
agent_image:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
creator_avatar:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
agent_image: "/testing_agent_image.jpg",
|
||||
creator_avatar: "/testing_avatar.png",
|
||||
slug: "multitasker",
|
||||
},
|
||||
{
|
||||
@@ -87,10 +79,8 @@ const mockFeaturedAgents = [
|
||||
description: "Simple and efficient task automation tool.",
|
||||
runs: 50000,
|
||||
rating: 4.2,
|
||||
agent_image:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
creator_avatar:
|
||||
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
|
||||
agent_image: "/testing_agent_image.jpg",
|
||||
creator_avatar: "/testing_avatar.png",
|
||||
slug: "quicktask",
|
||||
},
|
||||
] satisfies StoreAgent[];
|
||||
@@ -98,36 +88,52 @@ const mockFeaturedAgents = [
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
featuredAgents: mockFeaturedAgents,
|
||||
// onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
|
||||
},
|
||||
};
|
||||
|
||||
export const SingleAgent: Story = {
|
||||
args: {
|
||||
featuredAgents: [mockFeaturedAgents[0]],
|
||||
// onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
|
||||
},
|
||||
};
|
||||
|
||||
export const NoAgents: Story = {
|
||||
args: {
|
||||
featuredAgents: [],
|
||||
// onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInteraction: Story = {
|
||||
export const WithManyAgents: Story = {
|
||||
args: {
|
||||
featuredAgents: Array(20)
|
||||
.fill(null)
|
||||
.map((_, i) => ({
|
||||
...mockFeaturedAgents[i % mockFeaturedAgents.length],
|
||||
agent_name: `Agent ${i + 1}: ${mockFeaturedAgents[i % mockFeaturedAgents.length].agent_name}`,
|
||||
slug: `agent-${i + 1}`,
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCardInteraction: Story = {
|
||||
args: {
|
||||
featuredAgents: mockFeaturedAgents,
|
||||
// onCardClick: (agentName: string) => console.log(`Clicked on ${agentName}`),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const featuredCard = canvas.getByText(
|
||||
"Personalized Morning Coffee Newsletter example of three lines",
|
||||
);
|
||||
|
||||
await userEvent.hover(featuredCard);
|
||||
await userEvent.click(featuredCard);
|
||||
// Find and interact with first card
|
||||
const cards = canvas.getAllByTestId("featured-store-card");
|
||||
await expect(cards.length).toBeGreaterThan(0);
|
||||
|
||||
const firstCard = cards[0];
|
||||
await userEvent.hover(firstCard);
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
|
||||
// Check that link is present and clickable
|
||||
const cardLink = firstCard.closest("a");
|
||||
await expect(cardLink).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(firstCard);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,9 +15,9 @@ 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
|
||||
"bg-violet-100 hover:bg-violet-200 dark:bg-violet-800", // #ddd6fe / #5b21b6
|
||||
"bg-blue-100 hover:bg-blue-200 dark:bg-blue-800", // #bfdbfe / #1e3a8a
|
||||
"bg-green-100 hover:bg-green-200 dark:bg-green-800", // #bbf7d0 / #065f46
|
||||
];
|
||||
|
||||
interface FeaturedSectionProps {
|
||||
@@ -46,22 +46,22 @@ export const FeaturedSection: React.FC<FeaturedSectionProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="w-full">
|
||||
<h2 className="mb-8 font-poppins text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
|
||||
<section className="w-full space-y-7">
|
||||
<h2 className="pl-4 font-poppins text-base font-medium text-zinc-500 md:pl-10">
|
||||
Featured agents
|
||||
</h2>
|
||||
|
||||
<Carousel
|
||||
opts={{
|
||||
align: "center",
|
||||
align: "start",
|
||||
containScroll: "trimSnaps",
|
||||
}}
|
||||
>
|
||||
<CarouselContent>
|
||||
<CarouselContent className="p-0">
|
||||
{featuredAgents.map((agent, index) => (
|
||||
<CarouselItem
|
||||
key={index}
|
||||
className="h-[480px] md:basis-1/2 lg:basis-1/3"
|
||||
className={`flex w-screen flex-none items-center justify-center md:w-fit ${index === featuredAgents.length - 1 ? "md:mr-4" : ""} ${index === 0 ? "pl-8 md:pl-14" : ""}`}
|
||||
>
|
||||
<Link
|
||||
href={`/marketplace/agent/${encodeURIComponent(agent.creator)}/${encodeURIComponent(agent.slug)}`}
|
||||
@@ -75,10 +75,16 @@ export const FeaturedSection: React.FC<FeaturedSectionProps> = ({
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<div className="relative mt-4">
|
||||
<div className="relative mt-4 px-4 md:px-10">
|
||||
<CarouselIndicator />
|
||||
<CarouselPrevious afterClick={handlePrevSlide} />
|
||||
<CarouselNext afterClick={handleNextSlide} />
|
||||
<CarouselPrevious
|
||||
afterClick={handlePrevSlide}
|
||||
data-testid="Next slide Button"
|
||||
/>
|
||||
<CarouselNext
|
||||
afterClick={handleNextSlide}
|
||||
data-testid="Previous slide Button"
|
||||
/>
|
||||
</div>
|
||||
</Carousel>
|
||||
</section>
|
||||
|
||||
@@ -3,15 +3,15 @@ import { HeroSection } from "./HeroSection";
|
||||
import { userEvent, within, expect } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Composite/Hero Section",
|
||||
title: "Agpt UI/marketing/Hero Section",
|
||||
component: HeroSection,
|
||||
parameters: {
|
||||
layout: {
|
||||
center: true,
|
||||
fullscreen: true,
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex items-center justify-center py-4 md:p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
onSearch: { action: "searched" },
|
||||
@@ -38,7 +38,7 @@ export const WithInteraction: Story = {
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const searchInput = canvas.getByRole("store-search-input");
|
||||
const searchInput = canvas.getByRole("textbox");
|
||||
|
||||
await userEvent.type(searchInput, "test query");
|
||||
await userEvent.keyboard("{Enter}");
|
||||
@@ -47,8 +47,6 @@ export const WithInteraction: Story = {
|
||||
|
||||
const filterChip = canvas.getByText("Marketing");
|
||||
await userEvent.click(filterChip);
|
||||
|
||||
await expect(filterChip).toHaveClass("text-[#474747]");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -60,7 +58,7 @@ export const EmptySearch: Story = {
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const searchInput = canvas.getByRole("store-search-input");
|
||||
const searchInput = canvas.getByRole("textbox");
|
||||
|
||||
await userEvent.click(searchInput);
|
||||
await userEvent.keyboard("{Enter}");
|
||||
@@ -68,3 +66,20 @@ export const EmptySearch: Story = {
|
||||
await expect(searchInput).toHaveValue("");
|
||||
},
|
||||
};
|
||||
|
||||
export const FilterInteraction: Story = {
|
||||
args: {
|
||||
onSearch: (query: string) => console.log(`Searched: ${query}`),
|
||||
onFilterChange: (selectedFilters: string[]) =>
|
||||
console.log(`Filters changed: ${selectedFilters.join(", ")}`),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const filterChips = canvas.getAllByTestId("filter-chip");
|
||||
|
||||
for (const chip of filterChips) {
|
||||
await userEvent.click(chip);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,46 +21,31 @@ export const HeroSection: React.FC = () => {
|
||||
}
|
||||
|
||||
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">
|
||||
<div className="mb-4 text-center md:mb-8">
|
||||
<h1 className="text-center">
|
||||
<span className="font-poppins text-[48px] font-semibold leading-[54px] text-neutral-950 dark:text-neutral-50">
|
||||
Explore AI agents built for{" "}
|
||||
</span>
|
||||
<span className="font-poppins text-[48px] font-semibold leading-[54px] text-violet-600">
|
||||
you
|
||||
</span>
|
||||
<br />
|
||||
<span className="font-poppins text-[48px] font-semibold leading-[54px] text-neutral-950 dark:text-neutral-50">
|
||||
by the{" "}
|
||||
</span>
|
||||
<span className="font-poppins text-[48px] font-semibold leading-[54px] text-blue-500">
|
||||
community
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
<h3 className="mb:text-2xl mb-6 text-center font-sans text-xl font-normal leading-loose text-neutral-700 dark:text-neutral-300 md:mb-12">
|
||||
Bringing you AI agents designed by thinkers from around the world
|
||||
</h3>
|
||||
<div className="mb-4 flex justify-center sm:mb-5">
|
||||
<SearchBar height="h-[74px]" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-center">
|
||||
<FilterChips
|
||||
badges={[
|
||||
"Marketing",
|
||||
"SEO",
|
||||
"Content Creation",
|
||||
"Automation",
|
||||
"Fun",
|
||||
]}
|
||||
onFilterChange={onFilterChange}
|
||||
multiSelect={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto flex w-full flex-col items-center justify-center pb-20 pt-6 md:w-full md:pb-32 md:pt-36">
|
||||
{/* Title */}
|
||||
<h1 className="mb-4 text-center font-poppins text-3xl font-semibold leading-8 text-zinc-900 md:mb-8 md:text-[2.75rem] md:leading-[3.5rem]">
|
||||
<span>Explore AI agents built for </span>
|
||||
<span className="text-violet-600">you</span>
|
||||
<br />
|
||||
<span>by the </span>
|
||||
<span className="text-blue-500">community</span>
|
||||
</h1>
|
||||
|
||||
{/* Description */}
|
||||
<h3 className="mb-6 text-center font-sans text-base text-zinc-600 md:mb-12 md:text-xl">
|
||||
Bringing you AI agents designed by thinkers from around the world
|
||||
</h3>
|
||||
|
||||
{/* Seach bar */}
|
||||
<SearchBar className="max-w-4xl" />
|
||||
|
||||
{/* Filter chips */}
|
||||
<div className="mt-5">
|
||||
<FilterChips
|
||||
badges={["Marketing", "Content Creation", "SEO", "Automation", "Fun"]}
|
||||
onFilterChange={onFilterChange}
|
||||
multiSelect={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { PublishAgentPopout } from "@/components/agptui/composite/PublishAgentPopout";
|
||||
import { userEvent, within, expect } from "@storybook/test";
|
||||
|
||||
const meta = {
|
||||
title: "AGPT UI/Composite/Publish Agent Popout",
|
||||
component: PublishAgentPopout,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
trigger: { control: "object" },
|
||||
},
|
||||
} satisfies Meta<typeof PublishAgentPopout>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const WithCustomTrigger: Story = {
|
||||
args: {
|
||||
trigger: <button>Custom Publish Button</button>,
|
||||
},
|
||||
};
|
||||
|
||||
export const PublishFlow: Story = {
|
||||
args: {},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Open popout
|
||||
const publishButton = canvas.getByText("Publish Agent");
|
||||
await userEvent.click(publishButton);
|
||||
|
||||
// Select an agent (assuming one exists)
|
||||
const agentCard = await canvas.findByRole("button", {
|
||||
name: /select agent/i,
|
||||
});
|
||||
await userEvent.click(agentCard);
|
||||
|
||||
// Click next
|
||||
const nextButton = canvas.getByText("Next");
|
||||
await userEvent.click(nextButton);
|
||||
|
||||
// Fill out info form
|
||||
// Note: Actual form interactions would need to be added based on PublishAgentInfo implementation
|
||||
},
|
||||
};
|
||||
@@ -1,12 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
PopoverAnchor,
|
||||
} from "@/components/ui/popover";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { PublishAgentSelect } from "../PublishAgentSelect";
|
||||
import {
|
||||
PublishAgentInfo,
|
||||
@@ -21,6 +16,8 @@ import {
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import AutogptButton from "../AutogptButton";
|
||||
|
||||
interface PublishAgentPopoutProps {
|
||||
trigger?: React.ReactNode;
|
||||
openPopout?: boolean;
|
||||
@@ -69,7 +66,6 @@ export const PublishAgentPopout: React.FC<PublishAgentPopoutProps> = ({
|
||||
>(null);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const popupId = React.useId();
|
||||
const router = useRouter();
|
||||
const api = useBackendAPI();
|
||||
|
||||
@@ -209,98 +205,65 @@ export const PublishAgentPopout: React.FC<PublishAgentPopoutProps> = ({
|
||||
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>
|
||||
<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")}
|
||||
/>
|
||||
);
|
||||
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>
|
||||
<PublishAgentInfo
|
||||
onBack={handleBack}
|
||||
onSubmit={handleNextFromInfo}
|
||||
onClose={handleClose}
|
||||
initialData={initialData}
|
||||
/>
|
||||
);
|
||||
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>
|
||||
<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();
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (isOpen !== open) {
|
||||
setOpen(isOpen);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger 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"
|
||||
>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="h-screen w-screen max-w-full overflow-auto rounded-none border-none bg-black/40 backdrop-blur-[0.375rem]">
|
||||
{renderContent()}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
interface ProfileColorContainerProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const PageStructureContainer: React.FC<ProfileColorContainerProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
const backgroundMap = {
|
||||
"/profile": "bg-zinc-50",
|
||||
"/library": "bg-gray-100",
|
||||
};
|
||||
|
||||
const bgClass =
|
||||
Object.entries(backgroundMap).find(([path]) =>
|
||||
pathname.includes(path),
|
||||
)?.[1] || "bg-white";
|
||||
|
||||
return (
|
||||
<div className={bgClass}>
|
||||
<div className="mx-auto max-w-[1500px]">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageStructureContainer;
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
NotificationPreference,
|
||||
NotificationPreferenceDTO,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import AutogptInput from "@/components/agptui/AutogptInput";
|
||||
import AutogptButton from "@/components/agptui/AutogptButton";
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
@@ -119,10 +121,10 @@ export default function SettingsForm({ user, preferences }: SettingsFormProps) {
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-8"
|
||||
className="flex flex-col gap-10"
|
||||
>
|
||||
{/* Account Settings Section */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex max-w-3xl flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
@@ -130,7 +132,7 @@ export default function SettingsForm({ user, preferences }: SettingsFormProps) {
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="email" />
|
||||
<AutogptInput {...field} type="email" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -144,7 +146,7 @@ export default function SettingsForm({ user, preferences }: SettingsFormProps) {
|
||||
<FormItem>
|
||||
<FormLabel>New Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
<AutogptInput
|
||||
{...field}
|
||||
type="password"
|
||||
placeholder="************"
|
||||
@@ -162,7 +164,7 @@ export default function SettingsForm({ user, preferences }: SettingsFormProps) {
|
||||
<FormItem>
|
||||
<FormLabel>Confirm New Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
<AutogptInput
|
||||
{...field}
|
||||
type="password"
|
||||
placeholder="************"
|
||||
@@ -174,11 +176,13 @@ export default function SettingsForm({ user, preferences }: SettingsFormProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<Separator className="bg-neutral-300" />
|
||||
|
||||
{/* Notifications Section */}
|
||||
<div className="flex flex-col gap-6">
|
||||
<h3 className="text-lg font-medium">Notifications</h3>
|
||||
<div className="flex max-w-3xl flex-col gap-6">
|
||||
<h3 className="font-poppins text-base font-medium text-neutral-900">
|
||||
Notifications
|
||||
</h3>
|
||||
|
||||
{/* Agent Notifications */}
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -379,22 +383,25 @@ export default function SettingsForm({ user, preferences }: SettingsFormProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="bg-neutral-300" />
|
||||
|
||||
{/* Form Actions */}
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button
|
||||
<AutogptButton
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
</AutogptButton>
|
||||
<AutogptButton
|
||||
variant={"default"}
|
||||
type="submit"
|
||||
disabled={form.formState.isSubmitting || !form.formState.isDirty}
|
||||
>
|
||||
{form.formState.isSubmitting ? "Saving..." : "Save changes"}
|
||||
</Button>
|
||||
</AutogptButton>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -26,10 +26,6 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const SingleSelection: Story = {
|
||||
args: {
|
||||
mode: "single",
|
||||
@@ -56,15 +52,3 @@ export const RangeSelection: Story = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const HideOutsideDays: Story = {
|
||||
args: {
|
||||
showOutsideDays: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomClassName: Story = {
|
||||
args: {
|
||||
className: "border rounded-lg shadow-lg",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -63,8 +63,7 @@ function Calendar({
|
||||
range_end: "range-end",
|
||||
selected:
|
||||
"bg-neutral-900 text-neutral-100 hover:bg-neutral-900 hover:text-neutral-50 focus:bg-neutral-700 focus:text-neutral-50 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50 dark:hover:text-neutral-900 dark:focus:bg-neutral-50 dark:focus:text-neutral-900",
|
||||
today:
|
||||
"bg-neutral-100 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-50",
|
||||
today: "bg-neutral-100 dark:bg-neutral-800 dark:text-neutral-50",
|
||||
outside:
|
||||
"day-outside text-neutral-500 opacity-50 aria-selected:bg-neutral-100/50 aria-selected:text-neutral-500 aria-selected:opacity-30 dark:text-neutral-400 dark:aria-selected:bg-neutral-800/50 dark:aria-selected:text-neutral-400",
|
||||
disabled: "text-neutral-500 opacity-50 dark:text-neutral-400",
|
||||
|
||||
@@ -186,7 +186,7 @@ const CarouselItem = React.forwardRef<
|
||||
aria-roledescription="slide"
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
orientation === "horizontal" ? "pl-5" : "pt-5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -211,9 +211,9 @@ const CarouselPrevious = React.forwardRef<
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-[52px] w-[52px] rounded-full",
|
||||
"pointer absolute h-10 w-10 rounded-full border border-zinc-700 bg-white text-zinc-800 hover:bg-zinc-800 hover:text-white",
|
||||
orientation === "horizontal"
|
||||
? "right-20 top-0"
|
||||
? "right-18 top-0 md:right-24"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className,
|
||||
)}
|
||||
@@ -226,7 +226,7 @@ const CarouselPrevious = React.forwardRef<
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeft className="h-8 w-8" strokeWidth={1.25} />
|
||||
<ChevronLeft className="h-5 w-5" strokeWidth={1.25} />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
);
|
||||
@@ -257,9 +257,9 @@ const CarouselNext = React.forwardRef<
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute h-[52px] w-[52px] rounded-full",
|
||||
"order absolute h-10 w-10 rounded-full border-zinc-700 bg-white text-zinc-800 hover:bg-zinc-800 hover:text-white",
|
||||
orientation === "horizontal"
|
||||
? "right-4 top-0"
|
||||
? "right-6 top-0 md:right-12"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className,
|
||||
)}
|
||||
@@ -267,7 +267,7 @@ const CarouselNext = React.forwardRef<
|
||||
onClick={handleClick}
|
||||
{...props}
|
||||
>
|
||||
<ChevronRight className="h-8 w-8" strokeWidth={1.25} />
|
||||
<ChevronRight className="h-5 w-5" strokeWidth={1.25} />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
);
|
||||
@@ -302,7 +302,7 @@ const CarouselIndicator = React.forwardRef<
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("relative top-7 flex h-3 items-center gap-2", className)}
|
||||
className={cn("relative top-3.5 flex h-3 items-center gap-2", className)}
|
||||
{...props}
|
||||
>
|
||||
{scrollSnaps.map((_, index) => (
|
||||
@@ -311,8 +311,8 @@ const CarouselIndicator = React.forwardRef<
|
||||
onClick={() => scrollTo(index)}
|
||||
className={cn(
|
||||
selectedIndex === index
|
||||
? "h-3 w-[52px] rounded-[39px] bg-neutral-800 transition-all duration-500 dark:bg-neutral-200"
|
||||
: "h-3 w-3 rounded-full bg-neutral-300 transition-all duration-500 dark:bg-neutral-600",
|
||||
? "h-3 w-[52px] rounded-[39px] bg-zinc-600 transition-all duration-500 dark:bg-neutral-200"
|
||||
: "h-3 w-3 rounded-full bg-zinc-300 transition-all duration-500 dark:bg-neutral-600",
|
||||
"cursor-pointer",
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -58,7 +58,7 @@ const TableRow = React.forwardRef<
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-neutral-100/50 data-[state=selected]:bg-neutral-100 dark:hover:bg-neutral-800/50 dark:data-[state=selected]:bg-neutral-800",
|
||||
"border-b border-neutral-300 transition-colors hover:bg-neutral-100/50 data-[state=selected]:bg-neutral-100 dark:hover:bg-neutral-800/50 dark:data-[state=selected]:bg-neutral-800",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -73,7 +73,7 @@ const TableHead = React.forwardRef<
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-neutral-500 dark:text-neutral-400 [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
"h-10 pr-2.5 text-left align-middle font-medium text-neutral-500 dark:text-neutral-400 [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -88,7 +88,7 @@ const TableCell = React.forwardRef<
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
"py-3 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user