Compare commits

...

72 Commits

Author SHA1 Message Date
Abhimanyu Yadav
feac4b7579 fix chromatic ui 2.0 2025-05-13 16:00:23 +05:30
Abhimanyu Yadav
90101604a7 fix chromatic ui 2025-05-13 15:54:27 +05:30
Abhimanyu Yadav
20d5845c72 Merge branch 'dev' into abhi/ci-chromatic 2025-05-13 15:45:12 +05:30
Abhimanyu Yadav
740a20fc87 fix lint 2025-05-13 15:42:39 +05:30
Abhimanyu Yadav
cc16ac103a testing something 2025-05-13 15:40:03 +05:30
Abhimanyu Yadav
b1528c28db fix toast 2025-05-13 08:46:27 +05:30
Abhimanyu Yadav
7c511ea0d3 rebuild chromatic 2025-05-13 08:31:47 +05:30
Abhimanyu Yadav
a922f73f3e fix calender ui 2025-05-13 08:16:50 +05:30
Abhimanyu Yadav
6425612e0b Merge branch 'dev' into abhi/ci-chromatic 2025-05-13 08:04:19 +05:30
Abhimanyu Yadav
c6cc00f9a7 Merge branch 'dev' into abhi/ci-chromatic 2025-05-10 21:48:10 +05:30
Abhimanyu Yadav
7449a378e5 Merge branch 'dev' into abhi/ci-chromatic 2025-05-09 21:44:17 +05:30
Abhimanyu Yadav
4fa44df262 Merge branch 'dev' into abhi/ci-chromatic 2025-05-08 11:45:11 +05:30
Abhimanyu Yadav
76db88730f Merge branch 'dev' into abhi/ci-chromatic 2025-05-07 14:28:25 +05:30
Abhimanyu Yadav
8eedfade11 fix format 2025-05-06 10:09:43 +05:30
Abhimanyu Yadav
fe472e2dfb add custom buttons on agent page 2025-05-06 10:04:38 +05:30
Abhimanyu Yadav
d93b6ecf59 Merge branch 'dev' into abhi/ci-chromatic 2025-05-06 09:54:00 +05:30
Abhimanyu Yadav
1acc000593 Merge branch 'dev' into abhi/ci-chromatic 2025-05-05 20:45:38 +05:30
Abhimanyu Yadav
fd345f2a3c add new default images for testing 2025-05-05 16:49:26 +05:30
Abhimanyu Yadav
249d61391e fix all stories 2025-05-05 16:49:10 +05:30
Abhimanyu Yadav
212c105acc fix login tests on chromium 2025-05-05 13:57:24 +05:30
Abhimanyu Yadav
5e7958ce60 fix ui tests 2025-05-05 11:50:58 +05:30
Abhimanyu Yadav
ac9b0ad263 fix all components 2025-05-05 11:46:11 +05:30
Abhimanyu Yadav
eadb3e88f9 Merge branch 'dev' into abhi/ci-chromatic 2025-05-05 09:11:47 +05:30
Abhimanyu Yadav
5d198e4fad fix layout of complete website 2025-05-05 09:11:10 +05:30
Abhimanyu Yadav
86cbd6edd1 Merge branch 'dev' into abhi/ci-chromatic 2025-05-02 09:06:46 +05:30
Abhimanyu Yadav
781d8eb8b4 Merge branch 'dev' into abhi/ci-chromatic 2025-05-01 18:05:43 +05:30
Abhimanyu Yadav
368df7bd3b fix all components of marketplace and profile according to new design 2025-05-01 18:05:25 +05:30
Abhimanyu Yadav
5ba94958d9 add new design changes in some components 2025-04-30 22:52:50 +05:30
Abhimanyu Yadav
2d5f541743 small fixes 2025-04-30 20:38:41 +05:30
Abhimanyu Yadav
234ce73868 Fix all tests of stories 2025-04-30 12:19:18 +05:30
Abhimanyu Yadav
a7926e1249 fix button tests 2025-04-30 11:45:29 +05:30
Abhimanyu Yadav
bb7c8ba419 fix all component of marketplace 2025-04-30 11:39:27 +05:30
Abhimanyu Yadav
3c2fb6a3c5 add new story for search filter chips 2025-04-30 09:57:16 +05:30
Abhimanyu Yadav
b499b6a896 add new story for search filter chips 2025-04-30 09:56:59 +05:30
Abhimanyu Yadav
4d6ef9fcd1 fix all Publish Components 2025-04-30 09:45:32 +05:30
Abhimanyu Yadav
cf015e80e3 Merge branch 'dev' into abhi/ci-chromatic 2025-04-30 08:50:56 +05:30
Abhimanyu Yadav
dc16964a81 fix lint 2025-04-28 17:01:04 +05:30
Abhimanyu Yadav
f4e0735d22 fix PublishAgentSelectInfo Component 2025-04-28 16:57:58 +05:30
Abhimanyu Yadav
36578e29c3 fix PublishAgentSelect and PublishAgentPopoutcomponent 2025-04-28 15:43:16 +05:30
Abhimanyu Yadav
a6fcb3abcd fix Hero section component and stories 2025-04-28 12:41:19 +05:30
Abhimanyu Yadav
653a7f475a fix mock client and Become a cretor section 2025-04-28 11:46:10 +05:30
Abhimanyu Yadav
50b2fc30a5 Merge branch 'dev' into abhi/ci-chromatic 2025-04-28 11:17:04 +05:30
Abhimanyu Yadav
c26b5dc611 fix search bar story 2025-04-26 11:39:09 +05:30
Abhimanyu Yadav
43a508d9a0 fix search bar 2025-04-26 11:38:48 +05:30
Abhimanyu Yadav
94f0f5177a fix Filter Chips 2025-04-26 11:26:09 +05:30
Abhimanyu Yadav
b3d4d81f76 fix FeaturedCreator stories 2025-04-26 11:12:05 +05:30
Abhimanyu Yadav
141384856d fix Creator card 2025-04-26 10:41:28 +05:30
Abhimanyu Yadav
35da036355 fix AgentSection and StoreCard 2025-04-26 10:18:48 +05:30
Abhimanyu Yadav
36bda1cd34 change default iamge 2025-04-26 09:19:38 +05:30
Abhimanyu Yadav
01557a6a68 Merge branch 'dev' into abhi/ci-chromatic 2025-04-26 09:18:08 +05:30
Abhimanyu Yadav
073f9f4d58 fix FeaturedSection components and story 2025-04-25 11:30:02 +05:30
Abhimanyu Yadav
6b44ca536c complete fix of AgentTable 2025-04-25 10:23:36 +05:30
Abhimanyu Yadav
4ef5c5ca20 Merge branch 'dev' into abhi/ci-chromatic 2025-04-25 09:01:26 +05:30
Zamil Majdy
f60d44ecb9 feat(frontend): Add billing page toggle (#9877)
### Changes 🏗️

Provide a system toggle for disabling the billing page:
NEXT_PUBLIC_SHOW_BILLING_PAGE

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Toggle `NEXT_PUBLIC_SHOW_BILLING_PAGE` value.
2025-04-25 09:00:41 +05:30
Zamil Majdy
c00d7a9f6d fix(block): Fix Smart Decision Block missing input beads & incompability with input in special characters (#9875)
Smart Decision Block was not able to work with sub agent with custom
name input & the bead were not properly propagated in the execution UI.
The scope of this PR is fixing it.

### Changes 🏗️

* Introduce an easy to parse format of tool edge:
`{tool}_^_{func}_~_{arg}`. Graph using SmartDecisionBlock needs to be
re-saved before execution to work.
* Reduce cluttering on a smart decision block logic.
* Fix beads not being shown for a smart decision block tool calling.

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Execute an SDM with some special character input as a tool

<img width="672" alt="image"
src="https://github.com/user-attachments/assets/873556b3-c16a-4dd1-ad84-bc86c636c406"
/>
2025-04-25 09:00:41 +05:30
Krzysztof Czerwinski
ee4ebc1956 feat(frontend): Update "Edit a copy" modal and buttons (#9876)
Update "Edit a copy" modal text when copying marketplace agent in
Library. Update agent action buttons to reflect the design accurately.

### Changes 🏗️

- Update modal text
- Disable copying owned agents (only marketplace allowed)
- `Open in Builder` -> `Customize agent`
- Disabled `Customize agent` instead of hiding
- Change `Delete agent` to non-destructive design

### Checklist 📋

#### For code changes:
- [ ] I have clearly listed my changes in the PR description
- [ ] I have made a test plan
- [ ] I have tested my changes according to the test plan:
  - [ ] ...
2025-04-25 09:00:41 +05:30
Krzysztof Czerwinski
bdbf8f2ffd fix(backend): Strip secrets, credentials when forking agent (#9874)
Strip secrets, credentials when forking agent

### Changes 🏗️

<!-- Concisely describe all of the changes made in this pull request:
-->

### Checklist 📋

#### For code changes:
- [ ] I have clearly listed my changes in the PR description
- [ ] I have made a test plan
- [ ] I have tested my changes according to the test plan:
  - [ ] ...
2025-04-25 09:00:41 +05:30
Toran Bruce Richards
0b224568e6 Update repo-close-stale-issues.yml 2025-04-25 09:00:41 +05:30
Zamil Majdy
d76e2ac1f6 feat(backend): Expose execution prometheus metrics (#9866)
Currently, we have no visibility on the state of the execution manager,
the scope of this PR is to open up the observability of it by exposing
Prometheus metrics.

### Changes 🏗️

Re-use the execution manager port to expose the Prometheus metrics.

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Hit /metrics on 8002 port
2025-04-25 09:00:41 +05:30
Zamil Majdy
5679d6eb7f feat(backend): Use forkserver on process creation if possible (#9864)
### Changes 🏗️

Set process starting mode to forkserver instead of spawn, if possible,
for performance benefits.

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Existing tests
2025-04-25 09:00:41 +05:30
Abhimanyu Yadav
dfa29b9b22 fixing Agent Table phase 1 2025-04-24 17:30:46 +05:30
Abhimanyu Yadav
fda57a423a fix AgentInfo component and its stories 2025-04-24 11:31:53 +05:30
Abhimanyu Yadav
dc7f482d0a fix unhandled request in FeaturedAgentCard 2025-04-24 10:23:11 +05:30
Abhimanyu Yadav
4fd11676f2 Merge branch 'dev' into abhi/ci-chromatic 2025-04-24 09:20:09 +05:30
Abhimanyu Yadav
335e4a695f rebuild all stories with new env 2025-04-24 09:19:32 +05:30
Abhimanyu Yadav
34345dbd47 fix missing supabase env secret error in chromatic by adding dummy
secrets
2025-04-23 18:41:15 +05:30
Abhimanyu Yadav
5e73bcc812 fix FeaturedAgent Component and its stories 2025-04-23 18:20:24 +05:30
Abhimanyu Yadav
a7d4de66c2 fix tailwind fonts in storybook 2025-04-23 16:03:58 +05:30
Abhimanyu Yadav
3989bdd175 fix storybook mock clients and add better documentaion 2025-04-23 11:29:56 +05:30
Abhimanyu Yadav
f341965e52 Merge branch 'dev' into abhi/ci-chromatic 2025-04-23 09:08:13 +05:30
Abhimanyu Yadav
fcf6f3b393 add mock backend provider in preview.tsx 2025-04-22 20:54:43 +05:30
Abhimanyu Yadav
52af83cf36 add chromatic in github ci 2025-04-22 20:47:27 +05:30
132 changed files with 2952 additions and 3628 deletions

View File

@@ -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:

View File

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

View File

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

View 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;

View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

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

View File

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

View File

@@ -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 (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
)}
>

View File

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

View File

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

View File

@@ -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",
],
},
};

View File

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

View File

@@ -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: [],
},
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -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" />

View File

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

View File

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

View File

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

View File

@@ -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");
},
},
};

View File

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

View File

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

View File

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

View File

@@ -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",
},
],
},
};

View File

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

View File

@@ -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 = {

View File

@@ -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",
},

View File

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

View 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>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: [],

View File

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

View File

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

View File

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

View File

@@ -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");
},
};

View File

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

View File

@@ -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");
},
};

View File

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

View File

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

View File

@@ -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={[

View File

@@ -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"
}`}
>

View File

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

View File

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

View File

@@ -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");
},
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -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&apos;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&apos;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>

View File

@@ -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",
],
},
},

View File

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

View File

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

View File

@@ -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");
},
};

View File

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

View File

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

View File

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

View File

@@ -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",

View File

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

View File

@@ -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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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");
},
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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",

View File

@@ -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",
)}
/>

View File

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