Compare commits

...

295 Commits

Author SHA1 Message Date
Bentlybro
05afc1e1bb search results - search box reduce height to 60px 2024-12-13 13:45:21 +00:00
Swifty
a597507680 Merge branch 'dev' into dev-store-v2 2024-12-13 13:13:25 +01:00
SwiftyOS
14c223435b added in other browsers 2024-12-13 13:11:13 +01:00
SwiftyOS
f6ad194306 fix profile tests 2024-12-13 13:10:19 +01:00
SwiftyOS
e5c21ceda9 fix login 2024-12-13 12:51:55 +01:00
SwiftyOS
1a4a3e72ec fixed build tests 2024-12-13 12:36:38 +01:00
SwiftyOS
584b121a16 types 2024-12-12 15:53:19 +01:00
SwiftyOS
d67c7a3704 update auth tests 2024-12-12 15:50:47 +01:00
SwiftyOS
dec9dfad9d fmt 2024-12-12 15:27:23 +01:00
SwiftyOS
4af2297ca4 fix schema migrations 2024-12-12 15:12:43 +01:00
SwiftyOS
72e9dd6b37 fix schema merge conflicts 2024-12-12 15:08:13 +01:00
SwiftyOS
987fe6d3b8 Merge remote-tracking branch 'origin/dev' into dev-store-v2 2024-12-12 15:01:05 +01:00
SwiftyOS
3c024be10c change to using settings config rather than dotenv 2024-12-12 14:48:59 +01:00
SwiftyOS
e508d90fa7 add try catch on credit api 2024-12-12 12:37:56 +01:00
SwiftyOS
91fb4f5c56 remove superfulus button 2024-12-11 16:13:09 +01:00
SwiftyOS
32c15434d2 comment out all old marketplace componets 2024-12-11 15:35:55 +01:00
SwiftyOS
6fcceeabdb formatting 2024-12-11 15:28:24 +01:00
SwiftyOS
b1468f779c Allow running of agents from the monitor page 2024-12-11 15:27:58 +01:00
SwiftyOS
ac03211c59 removed templates form ui 2024-12-11 14:46:06 +01:00
SwiftyOS
c35453f41e Merge branch 'dev-store-v2-runner' of github.com:Significant-Gravitas/AutoGPT into dev-store-v2-runner 2024-12-11 10:13:56 +01:00
SwiftyOS
07d732ece7 removing templates 2024-12-11 10:13:41 +01:00
SwiftyOS
54baa84c28 remove marketplace api 2024-12-11 10:13:41 +01:00
SwiftyOS
0fb33f6cb7 add healty page 2024-12-10 15:42:16 +01:00
SwiftyOS
afd809f68a re-enable launch darkly 2024-12-10 15:33:23 +01:00
SwiftyOS
a8ae006967 remove launch darkly 2024-12-10 15:12:42 +01:00
SwiftyOS
b7603f6053 Update Dockerfile to set HOSTNAME environment variable for development and production stages 2024-12-10 14:38:47 +01:00
SwiftyOS
c91099a5ef added dynamic to force page rendering 2024-12-10 14:14:48 +01:00
SwiftyOS
c7ae4cfbda fixes 2024-12-10 13:05:47 +01:00
SwiftyOS
b5e1c8075e removed unused font import 2024-12-10 13:03:40 +01:00
SwiftyOS
d14cecb954 fix import 2024-12-10 13:02:28 +01:00
SwiftyOS
9bc72305c5 fix market image 2024-12-10 12:22:19 +01:00
SwiftyOS
980c90c07c match base images 2024-12-10 12:21:28 +01:00
SwiftyOS
006e843461 fix docker build 2024-12-10 12:21:00 +01:00
SwiftyOS
452fe314f6 update docker build 2024-12-10 12:19:03 +01:00
SwiftyOS
b86b0c758e updated poetry lock file 2024-12-10 10:53:13 +01:00
SwiftyOS
f1053ff8b4 remove marketplace api 2024-12-10 10:13:45 +01:00
SwiftyOS
7a1176073d updated monitoring page 2024-12-10 10:01:12 +01:00
SwiftyOS
3145f3d59d linting 2024-12-09 17:24:11 +01:00
SwiftyOS
89e8fe3854 fixes 2024-12-09 17:23:47 +01:00
SwiftyOS
6c34f8bd96 update lock files 2024-12-09 17:07:49 +01:00
SwiftyOS
6ccf62c3a6 tweaks to work with monitoring page 2024-12-09 17:05:04 +01:00
SwiftyOS
0217b75d65 Merge remote-tracking branch 'origin/dev' into dev-store-v2 2024-12-09 17:02:30 +01:00
SwiftyOS
6a712fd6bb linting 2024-12-09 16:45:19 +01:00
SwiftyOS
f4244e5038 remove rating card 2024-12-09 16:42:30 +01:00
SwiftyOS
9c0e8750b0 remove unused global 2024-12-09 16:36:35 +01:00
Bently
14ecaf861e feat(frontend): Update fonts + updates to agent page (#8886)
Updates fonts on most pages to match the design more + updates the agent
page, needs some more finishing up but nearly done

### Updated video showing most pages


https://github.com/user-attachments/assets/d90de22f-f2bd-477c-b3e7-544b5a17f2b7

---------

Co-authored-by: SwiftyOS <craigswift13@gmail.com>
2024-12-09 16:01:18 +01:00
SwiftyOS
6d6ed348fa updated rating card 2024-12-09 15:58:42 +01:00
SwiftyOS
1d81c61b77 added provider back and updated imports in custom node 2024-12-09 10:36:59 +01:00
SwiftyOS
3da5fc2cac prevented importing the context whenever the client is used 2024-12-09 09:52:52 +01:00
SwiftyOS
8d8664a3ce remove lang and comment out backend provider 2024-12-09 09:47:22 +01:00
SwiftyOS
d201b653c8 fixing merge conflict 2024-12-05 13:29:08 +01:00
SwiftyOS
f77172ec82 update lock fie 2024-12-05 13:18:34 +01:00
SwiftyOS
ec072ad52a Merge remote-tracking branch 'origin/dev' into dev-store-v2 2024-12-05 13:07:02 +01:00
SwiftyOS
9b456612aa update lock files 2024-12-05 12:45:39 +01:00
SwiftyOS
3d0ec9c52a redirect to store as home page 2024-12-05 10:47:30 +01:00
SwiftyOS
50d08654f8 updating locale parsing 2024-12-05 10:05:28 +01:00
SwiftyOS
fa871073ca fix linting errors 2024-12-05 09:46:31 +01:00
SwiftyOS
ab4f4549d6 fixed type errors 2024-12-05 09:43:39 +01:00
SwiftyOS
4492995d6b graceful fail locale 2024-12-04 17:37:42 +01:00
SwiftyOS
6a736cc60f added loging headers 2024-12-04 15:42:00 +01:00
SwiftyOS
60417545f0 make store work wihout env vars 2024-12-04 15:32:18 +01:00
SwiftyOS
4f20f868c1 disabling request during build 2024-12-04 14:37:36 +01:00
SwiftyOS
2ed8347430 commented out generateStaticParams 2024-12-04 14:28:51 +01:00
Aarushi
12eb3b2937 remove experimental 2024-12-04 12:57:36 +00:00
Aarushi
d5580f8a94 prevent static gen 2024-12-04 12:41:08 +00:00
Aarushi
5b0ecfd26b update package.json 2024-12-04 10:59:19 +00:00
Aarushi
3b24b0ac0d skip tests during build 2024-12-04 10:52:46 +00:00
SwiftyOS
715d425579 delete pages 2024-12-04 10:44:34 +01:00
SwiftyOS
0eda63fa15 fixing ci 2024-12-04 10:24:20 +01:00
SwiftyOS
4e886bd6e9 fixing tests 2024-12-04 10:14:00 +01:00
SwiftyOS
f3168ea187 update docker compose test to use env vars 2024-12-04 10:09:40 +01:00
SwiftyOS
7fdfffdfcc Merge remote-tracking branch 'origin/dev' into dev-store-v2 2024-12-04 10:08:45 +01:00
SwiftyOS
8f4d552909 updated packages 2024-12-04 09:58:21 +01:00
SwiftyOS
303a55145d linting 2024-12-03 16:12:12 +01:00
SwiftyOS
f54cfee4a7 formatting 2024-12-03 16:10:30 +01:00
SwiftyOS
b3bd0f5d54 trying too hook up rating card 2024-12-03 12:11:50 +01:00
SwiftyOS
91aa371220 add api routes for reviewing agents 2024-12-03 11:24:40 +01:00
SwiftyOS
1ab19dcc56 Merge remote-tracking branch 'origin/dev' into dev-store-v2 2024-12-03 10:21:42 +01:00
SwiftyOS
30a047eac3 signup now redirects to edit profile page 2024-12-02 16:59:28 +01:00
SwiftyOS
f2387147c7 fix tests 2024-12-02 16:59:12 +01:00
SwiftyOS
0f77f931ab Merge branch 'swiftyos/open-1920-marketplace-home-components' of github.com:Significant-Gravitas/AutoGPT into swiftyos/open-1920-marketplace-home-components 2024-12-02 16:55:07 +01:00
SwiftyOS
fdb82eda38 Added deleting submissions and viewing the popout 2024-12-02 16:54:56 +01:00
Bently
53eee63161 feat(platform): Adds rating card (#8840)
This adds the rating card, currently i just make it show in every agent
page till we have better logic for it + we want to make it show on the
block builder when we are looking at some one else's agent from the
marketplace, this is to be implemented soon

The Tutorial and ? buttons will also be hidden on the market place pages
so ignore them showing for now

### Changes 🏗️

![image](https://github.com/user-attachments/assets/7bd0314e-6c47-4c58-a113-1ea421f8c257)

![image](https://github.com/user-attachments/assets/1b5f5244-9387-4bbc-ba52-0de1c9dcd3d6)
2024-12-02 11:17:22 +01:00
SwiftyOS
48d27c91d4 update lock files 2024-12-02 09:30:56 +01:00
SwiftyOS
39c9e4a76c Merge remote-tracking branch 'origin/dev' into swiftyos/open-1920-marketplace-home-components 2024-12-02 09:24:54 +01:00
SwiftyOS
b3fd8bbfb9 added option to change password in settings 2024-12-02 09:21:20 +01:00
SwiftyOS
a4b186cf81 add sorting of creators too 2024-11-29 16:16:47 +01:00
SwiftyOS
a971d59974 handle sorting of agents 2024-11-29 16:13:05 +01:00
SwiftyOS
9db8832d6b fixed agent images not showing 2024-11-29 16:03:21 +01:00
SwiftyOS
acb35c3926 tweak layout 2024-11-29 15:50:44 +01:00
SwiftyOS
cd9c4218b0 only show agents that have no store listing 2024-11-29 15:45:59 +01:00
SwiftyOS
3ccf0138b1 remove notification section of settings page 2024-11-29 15:32:57 +01:00
SwiftyOS
19ff8f324d normalise styling between agent and creator page 2024-11-29 15:30:53 +01:00
SwiftyOS
f445918abf comment out arrow buttons on breadcrumbs 2024-11-29 15:13:12 +01:00
SwiftyOS
5a4083d542 Added tooltip for credits button 2024-11-29 15:10:21 +01:00
SwiftyOS
1e66137c7e more dark mode colors 2024-11-29 10:09:26 +01:00
SwiftyOS
bb13157d18 formatting 2024-11-28 16:14:02 +01:00
SwiftyOS
37e6b6f385 Add dark mode to flow and utils 2024-11-28 16:13:40 +01:00
SwiftyOS
fabf742601 updated node hadles dark mode 2024-11-28 15:36:23 +01:00
SwiftyOS
100b667afc updated node input dark mode 2024-11-28 15:36:04 +01:00
SwiftyOS
da10f1a2df updated custom node dark mode 2024-11-28 15:35:51 +01:00
SwiftyOS
65ada3fb72 Update Save Control 2024-11-28 14:20:50 +01:00
SwiftyOS
bc277acf57 Control Panel Dark mode 2024-11-28 14:17:05 +01:00
SwiftyOS
52d19d084c Blocks control dark mode 2024-11-28 14:16:47 +01:00
SwiftyOS
86a10858bd formatting 2024-11-28 12:09:23 +01:00
SwiftyOS
f297e42be4 dark mode color 2024-11-28 12:09:04 +01:00
SwiftyOS
2bbac5714f Search results styling and filter functionality 2024-11-28 11:00:20 +01:00
SwiftyOS
e063f4bcda Navbar publish agent button hooked up 2024-11-28 10:12:01 +01:00
SwiftyOS
6c64c5b98f OPEN-2127 make tutorial button show only in the builder 2024-11-28 09:57:30 +01:00
SwiftyOS
5530db63de Fix styling on home page 2024-11-28 09:43:35 +01:00
SwiftyOS
4278ae61b0 hooked up become a creator 2024-11-28 09:00:32 +01:00
SwiftyOS
0c78edb592 Added logout 2024-11-28 09:00:23 +01:00
Bently
93d3bd3773 feat(platform): Tweaks to Search page (#8806)
* Update to make it properly show "No results found" and look better

* fix text spacing under SearchFilterChips

* add flex to spacing
2024-11-27 13:57:16 +01:00
Bently
c1c3fd4982 feat(platform): Updates to Search page (#8769)
Updates to search page
2024-11-27 11:55:51 +01:00
SwiftyOS
d4b69d864f formatting 2024-11-27 11:53:09 +01:00
SwiftyOS
db3284830a getting dashboard working 2024-11-27 11:52:50 +01:00
SwiftyOS
d16cf6cfeb add slug 2024-11-27 11:41:02 +01:00
SwiftyOS
3f3919a843 Create store agent now working 2024-11-27 11:38:10 +01:00
SwiftyOS
244171d748 wip creat agent 2024-11-26 17:37:28 +01:00
SwiftyOS
faa683b6e4 update lock file 2024-11-26 11:37:20 +01:00
SwiftyOS
9255759e1e Merge remote-tracking branch 'origin/dev' into swiftyos/open-1920-marketplace-home-components 2024-11-26 11:36:31 +01:00
SwiftyOS
abbed4051d dashboard updates 2024-11-26 11:31:05 +01:00
SwiftyOS
8c5380d4f9 fix creator page layout 2024-11-26 11:16:00 +01:00
SwiftyOS
cacc6e1f86 file uploads working 2024-11-26 11:12:46 +01:00
SwiftyOS
9616baf695 switch to gcs 2024-11-26 11:04:08 +01:00
Bently
518f196e6b feat(platform): Updates to Home page (#8753)
* feat(platform): Updates to Home page

* remove left over variant's
2024-11-25 15:13:26 +01:00
SwiftyOS
a9693b582f profile page working 2024-11-25 12:16:20 +01:00
SwiftyOS
64fcba3f3a updates 2024-11-25 11:08:21 +01:00
SwiftyOS
a53f3f0e0a dark mode tweaks 2024-11-21 13:27:26 +01:00
SwiftyOS
84cdf189f4 udpate z level so it is always ontop 2024-11-21 13:24:33 +01:00
SwiftyOS
610a5b9943 Added theme toggle, fixed navbar 2024-11-21 13:19:04 +01:00
SwiftyOS
18b5f2047c update poetry locl 2024-11-21 12:15:40 +01:00
SwiftyOS
17db193faa Merge branch 'dev' into swiftyos/open-1920-marketplace-home-components 2024-11-21 12:13:20 +01:00
SwiftyOS
78476630cd fix migrations 2024-11-20 12:47:35 +01:00
SwiftyOS
7a41f36b13 update lock file 2024-11-20 12:32:07 +01:00
SwiftyOS
c315b8e700 Merge branch 'dev' into swiftyos/open-1920-marketplace-home-components 2024-11-20 12:31:40 +01:00
Bently
f6c1bdccac feat(platform): Updates to agent dashboard (#8598)
* Updates to agent dashboard

* fix squished "select all" text in table

* fix table alignment

---------

Co-authored-by: Swifty <craigswift13@gmail.com>
2024-11-20 11:01:15 +01:00
Bently
afe5c12afb feat(platform): Updates to Agent Page (#8664)
Co-authored-by: Swifty <craigswift13@gmail.com>
2024-11-20 10:28:26 +01:00
Bently
65344b9783 feat(platform): Updates to Profile and Settings page (#8694) 2024-11-20 09:59:40 +01:00
SwiftyOS
9f71fb940d update poetry lock 2024-11-15 12:08:01 +01:00
SwiftyOS
28a327c57a merge in dev 2024-11-15 12:07:44 +01:00
SwiftyOS
6aba1bce62 fmt 2024-11-13 16:41:24 +01:00
SwiftyOS
5db220c568 tidy up 2024-11-13 16:40:56 +01:00
SwiftyOS
8e63a4a8d7 Add outline of the search results page 2024-11-13 16:37:37 +01:00
SwiftyOS
175f17b131 wip adding dashboard, profile and settings page 2024-11-13 15:15:26 +01:00
SwiftyOS
9aec1f51ed integrated navbar 2024-11-13 14:28:15 +01:00
SwiftyOS
760e2ff592 delete api pages 2024-11-13 09:58:45 +01:00
SwiftyOS
d17ea2d62a Merge branch 'swiftyos/open-1920-marketplace-home-components' of github.com:Significant-Gravitas/AutoGPT into swiftyos/open-1920-marketplace-home-components 2024-11-13 09:55:38 +01:00
SwiftyOS
6351ba7f5d update locks 2024-11-13 09:55:28 +01:00
SwiftyOS
a1ba3b1ac3 Merge branch 'dev' into swiftyos/open-1920-marketplace-home-components 2024-11-13 09:53:34 +01:00
Bently
d78b4d9ab4 feat(platform): Updates to Profile popout menu (#8629)
Co-authored-by: Swifty <craigswift13@gmail.com>
2024-11-12 16:31:44 +01:00
SwiftyOS
1292c85d2a Delete creator details 2024-11-12 13:43:39 +01:00
SwiftyOS
c44fd7332c update creator page 2024-11-12 13:35:50 +01:00
Bently
45a2826df8 feat(platform): Updates to Creator page (#8624)
feat(platform): Updates to creator page

Co-authored-by: Swifty <craigswift13@gmail.com>
2024-11-12 13:22:37 +01:00
SwiftyOS
abb8134761 update search url 2024-11-12 11:42:29 +01:00
SwiftyOS
c879599871 fix migration 2024-11-12 11:42:20 +01:00
SwiftyOS
a71b2a1de6 hooking up pages, added missing update at 2024-11-12 11:15:14 +01:00
SwiftyOS
38b20e6158 remove use client 2024-11-12 09:49:36 +01:00
SwiftyOS
a8a0da1e3c add pages and update links 2024-11-12 09:18:19 +01:00
SwiftyOS
d3e7aab796 end-to-end working 2024-11-11 15:37:10 +01:00
SwiftyOS
f539c24571 wip 2024-11-11 11:29:01 +01:00
SwiftyOS
2068073e8c propgate removal of graph parents 2024-11-11 10:10:26 +01:00
SwiftyOS
04d36194c9 add test data generator 2024-11-11 10:08:23 +01:00
SwiftyOS
e8eda51b27 updated execpetion handling 2024-11-11 09:13:27 +01:00
SwiftyOS
aa5d304a2e Merge branch 'dev' into swiftyos/open-1920-marketplace-home-components 2024-11-11 09:05:37 +01:00
SwiftyOS
9dab7c9132 fix url sacnitization 2024-11-08 14:09:20 +01:00
SwiftyOS
4562606c54 fix liniting formatting, and icon selection in the frontend 2024-11-08 14:07:35 +01:00
SwiftyOS
fa933ada85 fmt 2024-11-08 13:56:43 +01:00
SwiftyOS
bc1d11bf42 isort fmt 2024-11-08 13:36:09 +01:00
SwiftyOS
5ac2e7044e Merge branch 'swiftyos/open-1920-marketplace-home-components' of github.com:Significant-Gravitas/AutoGPT into swiftyos/open-1920-marketplace-home-components 2024-11-08 13:31:17 +01:00
SwiftyOS
7f741468dd add views and db tests 2024-11-08 13:31:10 +01:00
Swifty
715e4c7d73 Merge branch 'dev' into swiftyos/open-1920-marketplace-home-components 2024-11-08 12:58:13 +01:00
SwiftyOS
ef473bbc8d updated migration 2024-11-08 12:57:49 +01:00
SwiftyOS
43ac5e0343 formatting 2024-11-08 12:55:22 +01:00
SwiftyOS
da15408a35 add model tests 2024-11-08 12:54:07 +01:00
SwiftyOS
bd00338690 Merge branch 'swiftyos/open-1920-marketplace-home-components' of github.com:Significant-Gravitas/AutoGPT into swiftyos/open-1920-marketplace-home-components 2024-11-08 12:48:26 +01:00
SwiftyOS
c63ccb5bd9 Added backend 2024-11-08 12:46:54 +01:00
Swifty
0e9906ea65 Merge branch 'dev' into swiftyos/open-1920-marketplace-home-components 2024-11-07 18:34:17 +01:00
SwiftyOS
ff0e786202 Formatting and added migration 2024-11-07 18:32:44 +01:00
SwiftyOS
deee943c3a Refactoring the v2 api code 2024-11-07 18:30:17 +01:00
SwiftyOS
40f38fcb46 Adding api endpoints 2024-11-07 11:51:48 +01:00
SwiftyOS
35ec676f35 Merge branch 'swiftyos/open-1920-marketplace-home-components' of github.com:Significant-Gravitas/AutoGPT into swiftyos/open-1920-marketplace-home-components 2024-11-07 11:51:15 +01:00
SwiftyOS
c345a79962 Updated Schema to include user groups 2024-11-07 09:55:32 +01:00
Bently
b9c7d1a115 feat(platform): Add settings page (#8576)
Add settings input form and settings page
2024-11-07 09:43:56 +01:00
Bently
bf459a17ba feat(platform): Profile page v2 (#8518)
* Initial changes

* fix save button text

* fix nav bar moving off screen on profile page

* Update sidebar to use slider icon for settings + add slider icon to icons.tsx

* formatting

* remove ProfileNavBar and replace with original Navbar + added credits card to original navbar + fix navbar and sidebar on smaller screens for profile page

---------

Co-authored-by: Swifty <craigswift13@gmail.com>
2024-11-07 09:43:11 +01:00
SwiftyOS
15befae65f wip server 2024-11-06 16:15:46 +01:00
SwiftyOS
2340e9b3f5 Added AuthProvider 2024-11-06 13:48:30 +01:00
SwiftyOS
7044782689 update lock files 2024-11-06 10:23:44 +01:00
SwiftyOS
ef4776f697 revert changes to docker file 2024-11-06 10:17:23 +01:00
SwiftyOS
946ba02969 revent changes to docker file 2024-11-06 10:16:29 +01:00
SwiftyOS
9a6ff408b4 remove docker compose full file 2024-11-06 10:14:02 +01:00
SwiftyOS
c07ea4f63d remove diff 2024-11-06 10:13:06 +01:00
SwiftyOS
2af974b381 remove diff 2024-11-06 10:12:33 +01:00
SwiftyOS
7e6bdf8b04 Merge branch 'swiftyos/open-1993-add-rsc-and-mocking-of-api-to-storybooks' into swiftyos/open-1920-marketplace-home-components 2024-11-06 10:10:17 +01:00
SwiftyOS
707c485212 Merge remote-tracking branch 'origin/dev' into swiftyos/open-1920-marketplace-home-components 2024-11-06 09:59:16 +01:00
SwiftyOS
5145aa7609 adding store endpoints 2024-11-05 16:01:18 +01:00
SwiftyOS
496990c096 Merge branch 'docker-qol' into swiftyos/open-1993-add-rsc-and-mocking-of-api-to-storybooks 2024-11-05 10:30:00 +01:00
SwiftyOS
52de22469f added comments 2024-11-05 10:11:33 +01:00
SwiftyOS
c27f163623 removed all services that are not strictly necessary for running the platform 2024-11-05 10:09:07 +01:00
SwiftyOS
29a61abfe3 Merge branch 'swiftyos/open-1920-marketplace-home-components' into swiftyos/open-1993-add-rsc-and-mocking-of-api-to-storybooks 2024-11-04 11:47:10 +01:00
SwiftyOS
1580fb5fa7 update yarn lock again 2024-11-01 15:07:11 +00:00
SwiftyOS
b9763aa28a Merge branch 'swiftyos/open-1920-marketplace-home-components' of github.com:Significant-Gravitas/AutoGPT into swiftyos/open-1920-marketplace-home-components 2024-11-01 15:06:04 +00:00
SwiftyOS
f9eefae1ad update yarn lock 2024-11-01 15:05:48 +00:00
Swifty
d16c1f259b Merge branch 'dev' into swiftyos/open-1920-marketplace-home-components 2024-11-01 14:59:59 +00:00
SwiftyOS
f0650d1f76 dont want to lose thse 2024-10-30 17:04:43 +00:00
SwiftyOS
d9710ce1af Added RSC and update filter chips and search bar to work as expected 2024-10-29 11:45:59 +01:00
SwiftyOS
e5a4b9a5ac Merge branch 'swiftyos/open-1920-marketplace-home-components' of github.com:Significant-Gravitas/AutoGPT into swiftyos/open-1920-marketplace-home-components 2024-10-29 11:00:08 +01:00
Bently
ebad48481e feat(platform): Add Publish agent flow section (#8462)
* Publish agents select page

* updates to new design

* made agent selection be dynamic based on screen size

* add new line for no agents message

* add accessibility

* add Publish Agent Info screen

* add Publish Agent Awaiting Review page

* Fixes for smaller screen sizes

* update to use agptui/Button for buttons

* move svgs to components/ui/icons.tsx
2024-10-29 09:12:51 +01:00
Swifty
4503dab267 Merge branch 'dev' into swiftyos/open-1920-marketplace-home-components 2024-10-29 08:23:18 +01:00
SwiftyOS
d73acd13cb tweaking feature card 2024-10-28 15:55:19 +01:00
SwiftyOS
a955794acd updated featured store card 2024-10-28 11:56:27 +01:00
SwiftyOS
03d754cb50 update featured agent and formatting 2024-10-28 10:49:51 +01:00
SwiftyOS
8dc22e2a63 updated navbar styling 2024-10-28 10:02:56 +01:00
SwiftyOS
bacdc190e7 Updated hero section for new design 2024-10-28 09:57:17 +01:00
SwiftyOS
5943c75873 Merge remote-tracking branch 'origin/dev' into swiftyos/open-1920-marketplace-home-components 2024-10-28 09:36:12 +01:00
SwiftyOS
8db695932e storing notes on api schema 2024-10-24 16:13:41 +02:00
SwiftyOS
a70d6a5193 Merge remote-tracking branch 'origin/dev' into swiftyos/open-1920-marketplace-home-components 2024-10-24 11:55:40 +02:00
SwiftyOS
4869a8ce22 migrating project to nextjs style api usage 2024-10-24 11:54:22 +02:00
SwiftyOS
d5d9ecd71c Merge remote-tracking branch 'origin/dev' into swiftyos/open-1920-marketplace-home-components 2024-10-23 15:00:14 +02:00
SwiftyOS
52119eadc2 Update auth flow and added localisation 2024-10-23 12:48:03 +02:00
SwiftyOS
334b4d5ef9 Added correct redirects for auth login 2024-10-23 10:54:53 +02:00
SwiftyOS
d7fc2dfb46 Added dispaly name 2024-10-22 14:55:19 +02:00
SwiftyOS
120469c8bf verticaly centered the icon and text in popout menu items 2024-10-22 14:05:32 +02:00
SwiftyOS
259d6f2b69 Add login / logout functionality to navbar 2024-10-22 13:55:43 +02:00
Swifty
dce436fb30 Merge branch 'dev' into swiftyos/open-1920-marketplace-home-components 2024-10-22 11:24:05 +02:00
SwiftyOS
44cb4e8e77 Add Creator Dashboard 2024-10-22 11:14:06 +02:00
SwiftyOS
88767a84d1 Add Agent table 2024-10-22 10:57:11 +02:00
SwiftyOS
8b03477d2d addded sidebar 2024-10-21 11:40:22 +02:00
SwiftyOS
10a2b36dc9 fix accessability 2024-10-21 11:19:51 +02:00
SwiftyOS
b1ccdacd98 cleaned up code 2024-10-21 11:03:37 +02:00
SwiftyOS
8811011286 Organise Stories 2024-10-21 09:34:59 +02:00
Swifty
b8c764ad70 Merge branch 'dev' into swiftyos/open-1920-marketplace-home-components 2024-10-21 09:26:28 +02:00
Swifty
4d69b2eb75 Merge branch 'dev' into swiftyos/open-1920-marketplace-home-components 2024-10-17 17:12:05 +02:00
SwiftyOS
5adc6c0a46 more pages wip 2024-10-17 17:01:46 +02:00
SwiftyOS
915b08d8a7 agent page 2024-10-17 16:43:26 +02:00
SwiftyOS
2997b12367 fix stories 2024-10-17 16:21:28 +02:00
SwiftyOS
82f0ee2240 fixinb avatar sizes 2024-10-17 16:20:05 +02:00
SwiftyOS
26bef8b918 wip 2024-10-17 16:14:40 +02:00
SwiftyOS
82f553ec0d finalsing home page 2024-10-17 12:58:59 +02:00
SwiftyOS
4ea85b5eaf finalised store page 2024-10-17 12:47:25 +02:00
SwiftyOS
45efe5a947 responsive tweaks 2024-10-17 12:42:05 +02:00
SwiftyOS
c2b320dd6a Updated home page 2024-10-17 12:32:00 +02:00
SwiftyOS
6b2d264414 fixed responsive issues 2024-10-17 12:31:50 +02:00
SwiftyOS
9a742cbe93 Added navbar 2024-10-17 12:31:28 +02:00
SwiftyOS
f96f2f101b Mobile Menu and accessability 2024-10-17 12:25:12 +02:00
SwiftyOS
6318a976b5 Added popout menu 2024-10-17 12:24:14 +02:00
SwiftyOS
c2c39a0cd6 updated icons 2024-10-17 12:23:53 +02:00
SwiftyOS
657e64d903 delete storybook publish ci 2024-10-16 15:15:32 +02:00
SwiftyOS
a2e681a09f fix tests 2024-10-16 14:59:23 +02:00
SwiftyOS
1fc3b2aa0a updated yarn lock 2024-10-16 12:39:55 +02:00
SwiftyOS
5465296ba6 updated workflow 2024-10-16 12:31:37 +02:00
SwiftyOS
fbfb8838fd added publishing of storybooks to chromatic 2024-10-16 12:29:13 +02:00
SwiftyOS
130261c75f removed chromatic-com visual testing to get rid of punycode bug 2024-10-16 12:18:38 +02:00
SwiftyOS
c8c954d862 npm audit updates 2024-10-16 10:29:19 +02:00
SwiftyOS
abfa707f69 updated page stories to have a wider selection of images 2024-10-16 09:57:10 +02:00
SwiftyOS
a01f326f7e added boring avatars 2024-10-16 09:56:50 +02:00
SwiftyOS
e377711c7e Added seperator 2024-10-16 09:34:16 +02:00
SwiftyOS
4c40b5f187 Update page to make it responsive 2024-10-15 13:47:56 +02:00
SwiftyOS
a8c5264e17 more responsive design work 2024-10-15 13:17:35 +02:00
SwiftyOS
9e8f76b749 changed featured card to carousel when mobile view 2024-10-15 12:15:25 +02:00
SwiftyOS
43f8ed55ea made features responsive 2024-10-15 11:25:20 +02:00
SwiftyOS
5d5f14c799 made responsive 2024-10-15 11:08:35 +02:00
SwiftyOS
9db01a7836 Made become a creator responsive 2024-10-15 11:04:07 +02:00
SwiftyOS
1e4a96883f Made creator card responsive and included image 2024-10-15 10:55:47 +02:00
SwiftyOS
a34dc25b34 formatting 2024-10-15 10:45:12 +02:00
SwiftyOS
90d3954e8c made featured store card responsive 2024-10-15 10:44:53 +02:00
SwiftyOS
aeb43b7d37 made filter chips responsive 2024-10-15 09:56:01 +02:00
SwiftyOS
7dedcaddb6 made breadcrumbs responsive 2024-10-15 09:49:58 +02:00
SwiftyOS
02463a5cb2 responsified the searchbar 2024-10-14 15:36:51 +02:00
SwiftyOS
4c18763e55 Merge branch 'swiftyos/open-1920-marketplace-home-components' of github.com:Significant-Gravitas/AutoGPT into swiftyos/open-1920-marketplace-home-components 2024-10-14 13:15:34 +02:00
SwiftyOS
8a9a1b59a4 agent infor 2024-10-14 13:15:30 +02:00
SwiftyOS
8d9b282376 agent images 2024-10-14 13:15:22 +02:00
SwiftyOS
ffdc457dea Breadcrumbs 2024-10-14 13:15:07 +02:00
SwiftyOS
015ac85a83 formatting 2024-10-14 13:14:58 +02:00
SwiftyOS
ee01a602ff responsive styling 2024-10-14 13:14:36 +02:00
SwiftyOS
f7f4207902 ui updates 2024-10-14 11:32:19 +02:00
Swifty
37bae48bd9 Merge branch 'master' into swiftyos/open-1920-marketplace-home-components 2024-10-14 10:01:23 +02:00
SwiftyOS
2ef21f929f added custom spacing and colors 2024-10-11 18:22:45 +02:00
SwiftyOS
6b77d71b88 formatting 2024-10-11 12:55:48 +02:00
SwiftyOS
e2946e0cd1 added navbar 2024-10-11 12:46:36 +02:00
SwiftyOS
887e7a4f0a added creator card 2024-10-11 12:46:22 +02:00
SwiftyOS
c4a8ab8a19 tweaked so spacing is added below the decsiption if the descriptiion is short 2024-10-11 12:31:56 +02:00
SwiftyOS
a3dda5d5a2 formatting 2024-10-11 12:26:02 +02:00
SwiftyOS
948279a67d Added filter chips 2024-10-11 12:25:49 +02:00
SwiftyOS
cf97e25b4a adding custom css styles 2024-10-11 12:25:17 +02:00
SwiftyOS
72e4eb2418 added font-neue 2024-10-11 12:05:28 +02:00
SwiftyOS
e241a4cc2a Added become a creator component 2024-10-11 12:01:48 +02:00
SwiftyOS
3accc65d44 Merge branch 'swiftyos/open-1920-marketplace-home-components' of github.com:Significant-Gravitas/AutoGPT into swiftyos/open-1920-marketplace-home-components 2024-10-11 11:51:26 +02:00
SwiftyOS
fd6f23f4a6 update styling 2024-10-11 11:51:20 +02:00
SwiftyOS
5ad56c553f Add avatar to the store card 2024-10-11 11:51:06 +02:00
Swifty
81471d8ffe Merge branch 'master' into swiftyos/open-1920-marketplace-home-components 2024-10-11 11:31:57 +02:00
SwiftyOS
e46bf6300e Added button 2024-10-11 11:31:39 +02:00
SwiftyOS
a0e87867b7 Added featured store card 2024-10-11 11:31:26 +02:00
SwiftyOS
00d5d843a2 Add store cards 2024-10-11 10:00:40 +02:00
SwiftyOS
3ce7cf2713 added search bar 2024-10-10 18:40:39 +02:00
196 changed files with 19877 additions and 6651 deletions

4
.gitignore vendored
View File

@@ -173,4 +173,6 @@ LICENSE.rtf
autogpt_platform/backend/settings.py
/.auth
/autogpt_platform/frontend/.auth
.test-contents
*.ign.*
.test-contents

View File

@@ -35,3 +35,12 @@ def verify_user(payload: dict | None, admin_only: bool) -> User:
raise fastapi.HTTPException(status_code=403, detail="Admin access required")
return User.from_payload(payload)
def get_user_id(payload: dict = fastapi.Depends(auth_middleware)) -> str:
user_id = payload.get("sub")
if not user_id:
raise fastapi.HTTPException(
status_code=401, detail="User ID not found in token"
)
return user_id

View File

@@ -6,18 +6,23 @@ ENV PYTHONUNBUFFERED 1
WORKDIR /app
RUN echo 'Acquire::http::Pipeline-Depth 0;\nAcquire::http::No-Cache true;\nAcquire::BrokenProxy true;\n' > /etc/apt/apt.conf.d/99fixbadproxy
RUN apt-get update --allow-releaseinfo-change --fix-missing
# Install build dependencies
RUN apt-get update \
&& apt-get install -y build-essential curl ffmpeg wget libcurl4-gnutls-dev libexpat1-dev libpq5 gettext libz-dev libssl-dev postgresql-client git \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
ENV POETRY_VERSION=1.8.3 \
POETRY_HOME="/opt/poetry" \
POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_CREATE=false \
PATH="$POETRY_HOME/bin:$PATH"
RUN apt-get install -y build-essential
RUN apt-get install -y libpq5
RUN apt-get install -y libz-dev
RUN apt-get install -y libssl-dev
RUN apt-get install -y postgresql-client
ENV POETRY_VERSION=1.8.3
ENV POETRY_HOME=/opt/poetry
ENV POETRY_NO_INTERACTION=1
ENV POETRY_VIRTUALENVS_CREATE=false
ENV PATH=/opt/poetry/bin:$PATH
# Upgrade pip and setuptools to fix security vulnerabilities
RUN pip3 install --upgrade pip setuptools
@@ -39,11 +44,11 @@ FROM python:3.11.10-slim-bookworm AS server_dependencies
WORKDIR /app
ENV POETRY_VERSION=1.8.3 \
POETRY_HOME="/opt/poetry" \
POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_CREATE=false \
PATH="$POETRY_HOME/bin:$PATH"
ENV POETRY_VERSION=1.8.3
ENV POETRY_HOME=/opt/poetry
ENV POETRY_NO_INTERACTION=1
ENV POETRY_VIRTUALENVS_CREATE=false
ENV PATH=/opt/poetry/bin:$PATH
# Upgrade pip and setuptools to fix security vulnerabilities

View File

@@ -143,7 +143,7 @@ class PineconeQueryBlock(Block):
top_k=input_data.top_k,
include_values=input_data.include_values,
include_metadata=input_data.include_metadata,
).to_dict()
).to_dict() # type: ignore
combined_text = ""
if results["matches"]:
texts = [

View File

@@ -160,7 +160,7 @@ def SchemaField(
exclude=exclude,
json_schema_extra=json_extra,
**kwargs,
)
) # type: ignore
class _BaseCredentials(BaseModel):

View File

@@ -16,6 +16,7 @@ import backend.data.db
import backend.data.graph
import backend.data.user
import backend.server.routers.v1
import backend.server.v2.store.routes
import backend.util.service
import backend.util.settings
@@ -84,7 +85,10 @@ def handle_internal_http_error(status_code: int = 500, log_error: bool = True):
app.add_exception_handler(ValueError, handle_internal_http_error(400))
app.add_exception_handler(Exception, handle_internal_http_error(500))
app.include_router(backend.server.routers.v1.v1_router, tags=["v1"])
app.include_router(backend.server.routers.v1.v1_router, tags=["v1"], prefix="/api")
app.include_router(
backend.server.v2.store.routes.router, tags=["v2"], prefix="/api/store"
)
@app.get(path="/health", tags=["health"], dependencies=[])

View File

@@ -69,8 +69,7 @@ integration_creds_manager = IntegrationCredentialsManager()
_user_credit_model = get_user_credit_model()
# Define the API routes
v1_router = APIRouter(prefix="/api")
v1_router = APIRouter()
v1_router.include_router(
backend.server.integrations.router.router,
@@ -132,7 +131,7 @@ def execute_graph_block(block_id: str, data: BlockInput) -> CompletedBlockOutput
@v1_router.get(path="/credits", dependencies=[Depends(auth_middleware)])
async def get_user_credits(
user_id: Annotated[str, Depends(get_user_id)]
user_id: Annotated[str, Depends(get_user_id)],
) -> dict[str, int]:
# Credits can go negative, so ensure it's at least 0 for user to see.
return {"credits": max(await _user_credit_model.get_or_refill_credit(user_id), 0)}

View File

@@ -0,0 +1,709 @@
import logging
from datetime import datetime
import random
import prisma.enums
import prisma.errors
import prisma.models
import prisma.types
import backend.server.v2.store.exceptions
import backend.server.v2.store.model
logger = logging.getLogger(__name__)
async def get_store_agents(
featured: bool = False,
creator: str | None = None,
sorted_by: str | None = None,
search_query: str | None = None,
category: str | None = None,
page: int = 1,
page_size: int = 20,
) -> backend.server.v2.store.model.StoreAgentsResponse:
logger.debug(
f"Getting store agents. featured={featured}, creator={creator}, sorted_by={sorted_by}, search={search_query}, category={category}, page={page}"
)
where_clause = {}
if featured:
where_clause["featured"] = featured
if creator:
where_clause["creator_username"] = creator
if category:
where_clause["categories"] = {"has": category}
if search_query:
where_clause["OR"] = [
{"agent_name": {"contains": search_query, "mode": "insensitive"}},
{"description": {"contains": search_query, "mode": "insensitive"}},
]
order_by = []
if sorted_by == "rating":
order_by.append({"rating": "desc"})
elif sorted_by == "runs":
order_by.append({"runs": "desc"})
elif sorted_by == "name":
order_by.append({"agent_name": "asc"})
try:
agents = await prisma.models.StoreAgent.prisma().find_many(
where=prisma.types.StoreAgentWhereInput(**where_clause),
order=order_by,
skip=(page - 1) * page_size,
take=page_size,
)
total = await prisma.models.StoreAgent.prisma().count(
where=prisma.types.StoreAgentWhereInput(**where_clause)
)
total_pages = (total + page_size - 1) // page_size
store_agents = [
backend.server.v2.store.model.StoreAgent(
slug=agent.slug,
agent_name=agent.agent_name,
agent_image=agent.agent_image[0] if agent.agent_image else "",
creator=agent.creator_username,
creator_avatar=agent.creator_avatar,
sub_heading=agent.sub_heading,
description=agent.description,
runs=agent.runs,
rating=agent.rating,
)
for agent in agents
]
logger.debug(f"Found {len(store_agents)} agents")
return backend.server.v2.store.model.StoreAgentsResponse(
agents=store_agents,
pagination=backend.server.v2.store.model.Pagination(
current_page=page,
total_items=total,
total_pages=total_pages,
page_size=page_size,
),
)
except Exception as e:
logger.error(f"Error getting store agents: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to fetch store agents"
) from e
async def get_store_agent_details(
username: str, agent_name: str
) -> backend.server.v2.store.model.StoreAgentDetails:
logger.debug(f"Getting store agent details for {username}/{agent_name}")
try:
agent = await prisma.models.StoreAgent.prisma().find_first(
where={"creator_username": username, "slug": agent_name}
)
if not agent:
logger.warning(f"Agent not found: {username}/{agent_name}")
raise backend.server.v2.store.exceptions.AgentNotFoundError(
f"Agent {username}/{agent_name} not found"
)
logger.debug(f"Found agent details for {username}/{agent_name}")
return backend.server.v2.store.model.StoreAgentDetails(
store_listing_version_id=agent.storeListingVersionId,
slug=agent.slug,
agent_name=agent.agent_name,
agent_video=agent.agent_video or "",
agent_image=agent.agent_image,
creator=agent.creator_username,
creator_avatar=agent.creator_avatar,
sub_heading=agent.sub_heading,
description=agent.description,
categories=agent.categories,
runs=agent.runs,
rating=agent.rating,
versions=agent.versions,
last_updated=agent.updated_at,
)
except backend.server.v2.store.exceptions.AgentNotFoundError:
raise
except Exception as e:
logger.error(f"Error getting store agent details: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to fetch agent details"
) from e
async def get_store_creators(
featured: bool = False,
search_query: str | None = None,
sorted_by: str | None = None,
page: int = 1,
page_size: int = 20,
) -> backend.server.v2.store.model.CreatorsResponse:
logger.debug(
f"Getting store creators. featured={featured}, search={search_query}, sorted_by={sorted_by}, page={page}"
)
# Build where clause
where = {}
# Add search filter if provided
if search_query:
where["OR"] = [
{"username": {"contains": search_query, "mode": "insensitive"}},
{"name": {"contains": search_query, "mode": "insensitive"}},
{"description": {"contains": search_query, "mode": "insensitive"}},
]
try:
# Get total count for pagination
total = await prisma.models.Creator.prisma().count(
where=prisma.types.CreatorWhereInput(**where)
)
total_pages = (total + page_size - 1) // page_size
# Add pagination
skip = (page - 1) * page_size
take = page_size
# Add sorting
order = []
if sorted_by == "agent_rating":
order.append({"agent_rating": "desc"})
elif sorted_by == "agent_runs":
order.append({"agent_runs": "desc"})
elif sorted_by == "num_agents":
order.append({"num_agents": "desc"})
else:
order.append({"username": "asc"})
# Execute query
creators = await prisma.models.Creator.prisma().find_many(
where=prisma.types.CreatorWhereInput(**where),
skip=skip,
take=take,
order=order,
)
# Convert to response model
creator_models = [
backend.server.v2.store.model.Creator(
username=creator.username,
name=creator.name,
description=creator.description,
avatar_url=creator.avatar_url,
num_agents=creator.num_agents,
agent_rating=creator.agent_rating,
agent_runs=creator.agent_runs,
)
for creator in creators
]
logger.debug(f"Found {len(creator_models)} creators")
return backend.server.v2.store.model.CreatorsResponse(
creators=creator_models,
pagination=backend.server.v2.store.model.Pagination(
current_page=page,
total_items=total,
total_pages=total_pages,
page_size=page_size,
),
)
except Exception as e:
logger.error(f"Error getting store creators: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to fetch store creators"
) from e
async def get_store_creator_details(
username: str,
) -> backend.server.v2.store.model.CreatorDetails:
logger.debug(f"Getting store creator details for {username}")
try:
# Query creator details from database
creator = await prisma.models.Creator.prisma().find_unique(
where={"username": username}
)
if not creator:
logger.warning(f"Creator not found: {username}")
raise backend.server.v2.store.exceptions.CreatorNotFoundError(
f"Creator {username} not found"
)
logger.debug(f"Found creator details for {username}")
return backend.server.v2.store.model.CreatorDetails(
name=creator.name,
username=creator.username,
description=creator.description,
links=creator.links,
avatar_url=creator.avatar_url,
agent_rating=creator.agent_rating,
agent_runs=creator.agent_runs,
top_categories=creator.top_categories,
)
except backend.server.v2.store.exceptions.CreatorNotFoundError:
raise
except Exception as e:
logger.error(f"Error getting store creator details: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to fetch creator details"
) from e
async def get_store_submissions(
user_id: str, page: int = 1, page_size: int = 20
) -> backend.server.v2.store.model.StoreSubmissionsResponse:
logger.debug(f"Getting store submissions for user {user_id}, page={page}")
try:
# Calculate pagination values
skip = (page - 1) * page_size
where = prisma.types.StoreSubmissionWhereInput(user_id=user_id)
# Query submissions from database
submissions = await prisma.models.StoreSubmission.prisma().find_many(
where=where, skip=skip, take=page_size, order=[{"date_submitted": "desc"}]
)
# Get total count for pagination
total = await prisma.models.StoreSubmission.prisma().count(where=where)
total_pages = (total + page_size - 1) // page_size
# Convert to response models
submission_models = [
backend.server.v2.store.model.StoreSubmission(
agent_id=sub.agent_id,
agent_version=sub.agent_version,
name=sub.name,
sub_heading=sub.sub_heading,
slug=sub.slug,
description=sub.description,
image_urls=sub.image_urls or [],
date_submitted=sub.date_submitted or datetime.now(),
status=sub.status,
runs=sub.runs or 0,
rating=sub.rating or 0.0,
)
for sub in submissions
]
logger.debug(f"Found {len(submission_models)} submissions")
return backend.server.v2.store.model.StoreSubmissionsResponse(
submissions=submission_models,
pagination=backend.server.v2.store.model.Pagination(
current_page=page,
total_items=total,
total_pages=total_pages,
page_size=page_size,
),
)
except Exception as e:
logger.error(f"Error fetching store submissions: {str(e)}")
# Return empty response rather than exposing internal errors
return backend.server.v2.store.model.StoreSubmissionsResponse(
submissions=[],
pagination=backend.server.v2.store.model.Pagination(
current_page=page,
total_items=0,
total_pages=0,
page_size=page_size,
),
)
async def delete_store_submission(
user_id: str,
submission_id: str,
) -> bool:
"""
Delete a store listing submission.
Args:
user_id: ID of the authenticated user
submission_id: ID of the submission to be deleted
Returns:
bool: True if the submission was successfully deleted, False otherwise
"""
logger.debug(f"Deleting store submission {submission_id} for user {user_id}")
try:
# Verify the submission belongs to this user
submission = await prisma.models.StoreListing.prisma().find_first(
where={"agentId": submission_id, "owningUserId": user_id}
)
if not submission:
logger.warning(f"Submission not found for user {user_id}: {submission_id}")
raise backend.server.v2.store.exceptions.SubmissionNotFoundError(
f"Submission not found for this user. User ID: {user_id}, Submission ID: {submission_id}"
)
# Delete the submission
await prisma.models.StoreListing.prisma().delete(
where=prisma.types.StoreListingWhereUniqueInput(id=submission.id)
)
logger.debug(
f"Successfully deleted submission {submission_id} for user {user_id}"
)
return True
except Exception as e:
logger.error(f"Error deleting store submission: {str(e)}")
return False
async def create_store_submission(
user_id: str,
agent_id: str,
agent_version: int,
slug: str,
name: str,
video_url: str | None = None,
image_urls: list[str] = [],
description: str = "",
sub_heading: str = "",
categories: list[str] = [],
) -> backend.server.v2.store.model.StoreSubmission:
"""
Create a new store listing submission.
Args:
user_id: ID of the authenticated user submitting the listing
agent_id: ID of the agent being submitted
agent_version: Version of the agent being submitted
slug: URL slug for the listing
name: Name of the agent
video_url: Optional URL to video demo
image_urls: List of image URLs for the listing
description: Description of the agent
categories: List of categories for the agent
Returns:
StoreSubmission: The created store submission
"""
logger.debug(
f"Creating store submission for user {user_id}, agent {agent_id} v{agent_version}"
)
try:
# First verify the agent belongs to this user
agent = await prisma.models.AgentGraph.prisma().find_first(
where=prisma.types.AgentGraphWhereInput(
id=agent_id, version=agent_version, userId=user_id
)
)
if not agent:
logger.warning(
f"Agent not found for user {user_id}: {agent_id} v{agent_version}"
)
raise backend.server.v2.store.exceptions.AgentNotFoundError(
f"Agent not found for this user. User ID: {user_id}, Agent ID: {agent_id}, Version: {agent_version}"
)
listing = await prisma.models.StoreListing.prisma().find_first(
where=prisma.types.StoreListingWhereInput(
agentId=agent_id, owningUserId=user_id
)
)
if listing is not None:
logger.warning(f"Listing already exists for agent {agent_id}")
raise backend.server.v2.store.exceptions.ListingExistsError(
"Listing already exists for this agent"
)
# Create the store listing
listing = await prisma.models.StoreListing.prisma().create(
data={
"agentId": agent_id,
"agentVersion": agent_version,
"owningUserId": user_id,
"createdAt": datetime.now(),
"StoreListingVersions": {
"create": {
"agentId": agent_id,
"agentVersion": agent_version,
"slug": slug,
"name": name,
"videoUrl": video_url,
"imageUrls": image_urls,
"description": description,
"categories": categories,
"subHeading": sub_heading,
}
},
}
)
logger.debug(f"Created store listing for agent {agent_id}")
# Return submission details
return backend.server.v2.store.model.StoreSubmission(
agent_id=agent_id,
agent_version=agent_version,
name=name,
slug=slug,
sub_heading=sub_heading,
description=description,
image_urls=image_urls,
date_submitted=listing.createdAt,
status=prisma.enums.SubmissionStatus.PENDING,
runs=0,
rating=0.0,
)
except (
backend.server.v2.store.exceptions.AgentNotFoundError,
backend.server.v2.store.exceptions.ListingExistsError,
):
raise
except prisma.errors.PrismaError as e:
logger.error(f"Database error creating store submission: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to create store submission"
) from e
async def create_store_review(
user_id: str,
store_listing_version_id: str,
score: int,
comments: str | None = None,
) -> backend.server.v2.store.model.StoreReview:
try:
review = await prisma.models.StoreListingReview.prisma().upsert(
where={
"storeListingVersionId_reviewByUserId": {
"storeListingVersionId": store_listing_version_id,
"reviewByUserId": user_id,
}
},
data={
"create": {
"reviewByUserId": user_id,
"storeListingVersionId": store_listing_version_id,
"score": score,
"comments": comments,
},
"update": {
"score": score,
"comments": comments,
},
},
)
return backend.server.v2.store.model.StoreReview(
score=review.score,
comments=review.comments,
)
except prisma.errors.PrismaError as e:
logger.error(f"Database error creating store review: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to create store review"
) from e
async def get_user_profile(
user_id: str,
) -> backend.server.v2.store.model.ProfileDetails:
logger.debug(f"Getting user profile for {user_id}")
try:
profile = await prisma.models.Profile.prisma().find_first(
where={"userId": user_id} # type: ignore
)
if not profile:
logger.warning(f"Profile not found for user {user_id}")
await prisma.models.Profile.prisma().create(
data=prisma.types.ProfileCreateInput(
userId=user_id,
name="No Profile Data",
username=f"{random.choice(['happy', 'clever', 'swift', 'bright', 'wise'])}-{random.choice(['fox', 'wolf', 'bear', 'eagle', 'owl'])}_{random.randint(1000,9999)}",
description="No Profile Data",
links=[],
avatarUrl="",
)
)
return backend.server.v2.store.model.ProfileDetails(
name="No Profile Data",
username="No Profile Data",
description="No Profile Data",
links=[],
avatar_url="",
)
return backend.server.v2.store.model.ProfileDetails(
name=profile.name,
username=profile.username,
description=profile.description,
links=profile.links,
avatar_url=profile.avatarUrl,
)
except Exception as e:
logger.error(f"Error getting user profile: {str(e)}")
return backend.server.v2.store.model.ProfileDetails(
name="No Profile Data",
username="No Profile Data",
description="No Profile Data",
links=[],
avatar_url="",
)
async def update_or_create_profile(
user_id: str, profile: backend.server.v2.store.model.Profile
) -> backend.server.v2.store.model.CreatorDetails:
"""
Update the store profile for a user. Creates a new profile if one doesn't exist.
Only allows updating if the user_id matches the owning user.
Args:
user_id: ID of the authenticated user
profile: Updated profile details
Returns:
CreatorDetails: The updated profile
Raises:
HTTPException: If user is not authorized to update this profile
"""
logger.debug(f"Updating profile for user {user_id}")
try:
# Check if profile exists for user
existing_profile = await prisma.models.Profile.prisma().find_first(
where={"userId": user_id}
)
# If no profile exists, create a new one
if not existing_profile:
logger.debug(f"Creating new profile for user {user_id}")
# Create new profile since one doesn't exist
new_profile = await prisma.models.Profile.prisma().create(
data={
"userId": user_id,
"name": profile.name,
"username": profile.username,
"description": profile.description,
"links": profile.links,
"avatarUrl": profile.avatar_url,
}
)
return backend.server.v2.store.model.CreatorDetails(
name=new_profile.name,
username=new_profile.username,
description=new_profile.description,
links=new_profile.links,
avatar_url=new_profile.avatarUrl or "",
agent_rating=0.0,
agent_runs=0,
top_categories=[],
)
else:
logger.debug(f"Updating existing profile for user {user_id}")
# Update the existing profile
updated_profile = await prisma.models.Profile.prisma().update(
where={"id": existing_profile.id},
data=prisma.types.ProfileUpdateInput(
name=profile.name,
username=profile.username,
description=profile.description,
links=profile.links,
avatarUrl=profile.avatar_url,
),
)
if updated_profile is None:
logger.error(f"Failed to update profile for user {user_id}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to update profile"
)
return backend.server.v2.store.model.CreatorDetails(
name=updated_profile.name,
username=updated_profile.username,
description=updated_profile.description,
links=updated_profile.links,
avatar_url=updated_profile.avatarUrl or "",
agent_rating=0.0,
agent_runs=0,
top_categories=[],
)
except prisma.errors.PrismaError as e:
logger.error(f"Database error updating profile: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to update profile"
) from e
async def get_my_agents(
user_id: str,
page: int = 1,
page_size: int = 20,
) -> backend.server.v2.store.model.MyAgentsResponse:
logger.debug(f"Getting my agents for user {user_id}, page={page}")
try:
agents_with_max_version = await prisma.models.AgentGraph.prisma().find_many(
where=prisma.types.AgentGraphWhereInput(
userId=user_id, StoreListing={"none": {"isDeleted": False}}
),
order=[{"version": "desc"}],
distinct=["id"],
skip=(page - 1) * page_size,
take=page_size,
)
# store_listings = await prisma.models.StoreListing.prisma().find_many(
# where=prisma.types.StoreListingWhereInput(
# isDeleted=False,
# ),
# )
total = len(
await prisma.models.AgentGraph.prisma().find_many(
where=prisma.types.AgentGraphWhereInput(
userId=user_id, StoreListing={"none": {"isDeleted": False}}
),
order=[{"version": "desc"}],
distinct=["id"],
)
)
total_pages = (total + page_size - 1) // page_size
agents = agents_with_max_version
my_agents = [
backend.server.v2.store.model.MyAgent(
agent_id=agent.id,
agent_version=agent.version,
agent_name=agent.name or "",
last_edited=agent.updatedAt or agent.createdAt,
)
for agent in agents
]
return backend.server.v2.store.model.MyAgentsResponse(
agents=my_agents,
pagination=backend.server.v2.store.model.Pagination(
current_page=page,
total_items=total,
total_pages=total_pages,
page_size=page_size,
),
)
except Exception as e:
logger.error(f"Error getting my agents: {str(e)}")
raise backend.server.v2.store.exceptions.DatabaseError(
"Failed to fetch my agents"
) from e

View File

@@ -0,0 +1,260 @@
from datetime import datetime
import prisma.errors
import prisma.models
import pytest
from prisma import Prisma
import backend.server.v2.store.db as db
from backend.server.v2.store.model import Profile
@pytest.fixture(autouse=True)
async def setup_prisma():
# Don't register client if already registered
try:
Prisma()
except prisma.errors.ClientAlreadyRegisteredError:
pass
yield
@pytest.mark.asyncio
async def test_get_store_agents(mocker):
# Mock data
mock_agents = [
prisma.models.StoreAgent(
listing_id="test-id",
storeListingVersionId="version123",
slug="test-agent",
agent_name="Test Agent",
agent_video=None,
agent_image=["image.jpg"],
featured=False,
creator_username="creator",
creator_avatar="avatar.jpg",
sub_heading="Test heading",
description="Test description",
categories=[],
runs=10,
rating=4.5,
versions=["1.0"],
updated_at=datetime.now(),
)
]
# Mock prisma calls
mock_store_agent = mocker.patch("prisma.models.StoreAgent.prisma")
mock_store_agent.return_value.find_many = mocker.AsyncMock(return_value=mock_agents)
mock_store_agent.return_value.count = mocker.AsyncMock(return_value=1)
# Call function
result = await db.get_store_agents()
# Verify results
assert len(result.agents) == 1
assert result.agents[0].slug == "test-agent"
assert result.pagination.total_items == 1
# Verify mocks called correctly
mock_store_agent.return_value.find_many.assert_called_once()
mock_store_agent.return_value.count.assert_called_once()
@pytest.mark.asyncio
async def test_get_store_agent_details(mocker):
# Mock data
mock_agent = prisma.models.StoreAgent(
listing_id="test-id",
storeListingVersionId="version123",
slug="test-agent",
agent_name="Test Agent",
agent_video="video.mp4",
agent_image=["image.jpg"],
featured=False,
creator_username="creator",
creator_avatar="avatar.jpg",
sub_heading="Test heading",
description="Test description",
categories=["test"],
runs=10,
rating=4.5,
versions=["1.0"],
updated_at=datetime.now(),
)
# Mock prisma call
mock_store_agent = mocker.patch("prisma.models.StoreAgent.prisma")
mock_store_agent.return_value.find_first = mocker.AsyncMock(return_value=mock_agent)
# Call function
result = await db.get_store_agent_details("creator", "test-agent")
# Verify results
assert result.slug == "test-agent"
assert result.agent_name == "Test Agent"
# Verify mock called correctly
mock_store_agent.return_value.find_first.assert_called_once_with(
where={"creator_username": "creator", "slug": "test-agent"}
)
@pytest.mark.asyncio
async def test_get_store_creator_details(mocker):
# Mock data
mock_creator_data = prisma.models.Creator(
name="Test Creator",
username="creator",
description="Test description",
links=["link1"],
avatar_url="avatar.jpg",
num_agents=1,
agent_rating=4.5,
agent_runs=10,
top_categories=["test"],
)
# Mock prisma call
mock_creator = mocker.patch("prisma.models.Creator.prisma")
mock_creator.return_value.find_unique = mocker.AsyncMock()
# Configure the mock to return values that will pass validation
mock_creator.return_value.find_unique.return_value = mock_creator_data
# Call function
result = await db.get_store_creator_details("creator")
# Verify results
assert result.username == "creator"
assert result.name == "Test Creator"
assert result.description == "Test description"
assert result.avatar_url == "avatar.jpg"
# Verify mock called correctly
mock_creator.return_value.find_unique.assert_called_once_with(
where={"username": "creator"}
)
@pytest.mark.asyncio
async def test_create_store_submission(mocker):
# Mock data
mock_agent = prisma.models.AgentGraph(
id="agent-id",
version=1,
userId="user-id",
createdAt=datetime.now(),
isActive=True,
isTemplate=False,
)
mock_listing = prisma.models.StoreListing(
id="listing-id",
createdAt=datetime.now(),
updatedAt=datetime.now(),
isDeleted=False,
isApproved=False,
agentId="agent-id",
agentVersion=1,
owningUserId="user-id",
)
# Mock prisma calls
mock_agent_graph = mocker.patch("prisma.models.AgentGraph.prisma")
mock_agent_graph.return_value.find_first = mocker.AsyncMock(return_value=mock_agent)
mock_store_listing = mocker.patch("prisma.models.StoreListing.prisma")
mock_store_listing.return_value.find_first = mocker.AsyncMock(return_value=None)
mock_store_listing.return_value.create = mocker.AsyncMock(return_value=mock_listing)
# Call function
result = await db.create_store_submission(
user_id="user-id",
agent_id="agent-id",
agent_version=1,
slug="test-agent",
name="Test Agent",
description="Test description",
)
# Verify results
assert result.name == "Test Agent"
assert result.description == "Test description"
# Verify mocks called correctly
mock_agent_graph.return_value.find_first.assert_called_once()
mock_store_listing.return_value.find_first.assert_called_once()
mock_store_listing.return_value.create.assert_called_once()
@pytest.mark.asyncio
async def test_update_profile(mocker):
# Mock data
mock_profile = prisma.models.Profile(
id="profile-id",
name="Test Creator",
username="creator",
description="Test description",
links=["link1"],
avatarUrl="avatar.jpg",
createdAt=datetime.now(),
updatedAt=datetime.now(),
)
# Mock prisma calls
mock_profile_db = mocker.patch("prisma.models.Profile.prisma")
mock_profile_db.return_value.find_first = mocker.AsyncMock(
return_value=mock_profile
)
mock_profile_db.return_value.update = mocker.AsyncMock(return_value=mock_profile)
# Test data
profile = Profile(
name="Test Creator",
username="creator",
description="Test description",
links=["link1"],
avatar_url="avatar.jpg",
)
# Call function
result = await db.update_or_create_profile("user-id", profile)
# Verify results
assert result.username == "creator"
assert result.name == "Test Creator"
# Verify mocks called correctly
mock_profile_db.return_value.find_first.assert_called_once()
mock_profile_db.return_value.update.assert_called_once()
@pytest.mark.asyncio
async def test_get_user_profile(mocker):
# Mock data
mock_profile = prisma.models.Profile(
id="profile-id",
name="No Profile Data",
username="testuser",
description="Test description",
links=["link1", "link2"],
avatarUrl="avatar.jpg",
createdAt=datetime.now(),
updatedAt=datetime.now(),
)
# Mock prisma calls
mock_profile_db = mocker.patch("prisma.models.Profile.prisma")
mock_profile_db.return_value.find_unique = mocker.AsyncMock(
return_value=mock_profile
)
# Call function
result = await db.get_user_profile("user-id")
# Verify results
assert result.name == "No Profile Data"
assert result.username == "No Profile Data"
assert result.description == "No Profile Data"
assert result.links == []
assert result.avatar_url == ""

View File

@@ -0,0 +1,76 @@
class MediaUploadError(Exception):
"""Base exception for media upload errors"""
pass
class InvalidFileTypeError(MediaUploadError):
"""Raised when file type is not supported"""
pass
class FileSizeTooLargeError(MediaUploadError):
"""Raised when file size exceeds maximum limit"""
pass
class FileReadError(MediaUploadError):
"""Raised when there's an error reading the file"""
pass
class StorageConfigError(MediaUploadError):
"""Raised when storage configuration is invalid"""
pass
class StorageUploadError(MediaUploadError):
"""Raised when upload to storage fails"""
pass
class StoreError(Exception):
"""Base exception for store-related errors"""
pass
class AgentNotFoundError(StoreError):
"""Raised when an agent is not found"""
pass
class CreatorNotFoundError(StoreError):
"""Raised when a creator is not found"""
pass
class ListingExistsError(StoreError):
"""Raised when trying to create a listing that already exists"""
pass
class DatabaseError(StoreError):
"""Raised when there is an error interacting with the database"""
pass
class ProfileNotFoundError(StoreError):
"""Raised when a profile is not found"""
pass
class SubmissionNotFoundError(StoreError):
"""Raised when a submission is not found"""
pass

View File

@@ -0,0 +1,101 @@
import logging
import os
import uuid
import fastapi
from google.cloud import storage
import backend.server.v2.store.exceptions
from backend.util.settings import Settings
logger = logging.getLogger(__name__)
ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"}
ALLOWED_VIDEO_TYPES = {"video/mp4", "video/webm"}
MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
async def upload_media(user_id: str, file: fastapi.UploadFile) -> str:
settings = Settings()
# Check required settings first before doing any file processing
if (
not settings.config.media_gcs_bucket_name
or not settings.config.google_application_credentials
):
logger.error("Missing required GCS settings")
raise backend.server.v2.store.exceptions.StorageConfigError(
"Missing storage configuration"
)
try:
# Validate file type
content_type = file.content_type
if (
content_type not in ALLOWED_IMAGE_TYPES
and content_type not in ALLOWED_VIDEO_TYPES
):
logger.warning(f"Invalid file type attempted: {content_type}")
raise backend.server.v2.store.exceptions.InvalidFileTypeError(
f"File type not supported. Must be jpeg, png, gif, webp, mp4 or webm. Content type: {content_type}"
)
# Validate file size
file_size = 0
chunk_size = 8192 # 8KB chunks
try:
while chunk := await file.read(chunk_size):
file_size += len(chunk)
if file_size > MAX_FILE_SIZE:
logger.warning(f"File size too large: {file_size} bytes")
raise backend.server.v2.store.exceptions.FileSizeTooLargeError(
"File too large. Maximum size is 50MB"
)
except backend.server.v2.store.exceptions.FileSizeTooLargeError:
raise
except Exception as e:
logger.error(f"Error reading file chunks: {str(e)}")
raise backend.server.v2.store.exceptions.FileReadError(
"Failed to read uploaded file"
) from e
# Reset file pointer
await file.seek(0)
# Generate unique filename
filename = file.filename or ""
file_ext = os.path.splitext(filename)[1].lower()
unique_filename = f"{uuid.uuid4()}{file_ext}"
# Construct storage path
media_type = "images" if content_type in ALLOWED_IMAGE_TYPES else "videos"
storage_path = f"users/{user_id}/{media_type}/{unique_filename}"
try:
storage_client = storage.Client()
bucket = storage_client.bucket(settings.config.media_gcs_bucket_name)
blob = bucket.blob(storage_path)
blob.content_type = content_type
file_bytes = await file.read()
blob.upload_from_string(file_bytes, content_type=content_type)
public_url = blob.public_url
logger.info(f"Successfully uploaded file to: {storage_path}")
return public_url
except Exception as e:
logger.error(f"GCS storage error: {str(e)}")
raise backend.server.v2.store.exceptions.StorageUploadError(
"Failed to upload file to storage"
) from e
except backend.server.v2.store.exceptions.MediaUploadError:
raise
except Exception as e:
logger.exception("Unexpected error in upload_media")
raise backend.server.v2.store.exceptions.MediaUploadError(
"Unexpected error during media upload"
) from e

View File

@@ -0,0 +1,107 @@
import io
import unittest.mock
import fastapi
import pytest
import starlette.datastructures
import backend.server.v2.store.exceptions
import backend.server.v2.store.media
from backend.util.settings import Settings
@pytest.fixture
def mock_settings(monkeypatch):
settings = Settings()
settings.config.media_gcs_bucket_name = "test-bucket"
settings.config.google_application_credentials = "test-credentials"
monkeypatch.setattr("backend.server.v2.store.media.Settings", lambda: settings)
return settings
@pytest.fixture
def mock_storage_client(mocker):
mock_client = unittest.mock.MagicMock()
mock_bucket = unittest.mock.MagicMock()
mock_blob = unittest.mock.MagicMock()
mock_client.bucket.return_value = mock_bucket
mock_bucket.blob.return_value = mock_blob
mock_blob.public_url = "http://test-url/media/test.jpg"
mocker.patch("google.cloud.storage.Client", return_value=mock_client)
return mock_client
async def test_upload_media_success(mock_settings, mock_storage_client):
test_file = fastapi.UploadFile(
filename="test.jpeg",
file=io.BytesIO(b"test data"),
headers=starlette.datastructures.Headers({"content-type": "image/jpeg"}),
)
result = await backend.server.v2.store.media.upload_media("test-user", test_file)
assert result == "http://test-url/media/test.jpg"
mock_bucket = mock_storage_client.bucket.return_value
mock_blob = mock_bucket.blob.return_value
mock_blob.upload_from_string.assert_called_once()
async def test_upload_media_invalid_type(mock_settings, mock_storage_client):
test_file = fastapi.UploadFile(
filename="test.txt",
file=io.BytesIO(b"test data"),
headers=starlette.datastructures.Headers({"content-type": "text/plain"}),
)
with pytest.raises(backend.server.v2.store.exceptions.InvalidFileTypeError):
await backend.server.v2.store.media.upload_media("test-user", test_file)
mock_bucket = mock_storage_client.bucket.return_value
mock_blob = mock_bucket.blob.return_value
mock_blob.upload_from_string.assert_not_called()
async def test_upload_media_missing_credentials(monkeypatch):
settings = Settings()
settings.config.media_gcs_bucket_name = ""
settings.config.google_application_credentials = ""
monkeypatch.setattr("backend.server.v2.store.media.Settings", lambda: settings)
test_file = fastapi.UploadFile(
filename="test.jpeg",
file=io.BytesIO(b"test data"),
headers=starlette.datastructures.Headers({"content-type": "image/jpeg"}),
)
with pytest.raises(backend.server.v2.store.exceptions.StorageConfigError):
await backend.server.v2.store.media.upload_media("test-user", test_file)
async def test_upload_media_video_type(mock_settings, mock_storage_client):
test_file = fastapi.UploadFile(
filename="test.mp4",
file=io.BytesIO(b"test video data"),
headers=starlette.datastructures.Headers({"content-type": "video/mp4"}),
)
result = await backend.server.v2.store.media.upload_media("test-user", test_file)
assert result == "http://test-url/media/test.jpg"
mock_bucket = mock_storage_client.bucket.return_value
mock_blob = mock_bucket.blob.return_value
mock_blob.upload_from_string.assert_called_once()
async def test_upload_media_file_too_large(mock_settings, mock_storage_client):
large_data = b"x" * (50 * 1024 * 1024 + 1) # 50MB + 1 byte
test_file = fastapi.UploadFile(
filename="test.jpeg",
file=io.BytesIO(large_data),
headers=starlette.datastructures.Headers({"content-type": "image/jpeg"}),
)
with pytest.raises(backend.server.v2.store.exceptions.FileSizeTooLargeError):
await backend.server.v2.store.media.upload_media("test-user", test_file)

View File

@@ -0,0 +1,150 @@
import datetime
from typing import List
import prisma.enums
import pydantic
class Pagination(pydantic.BaseModel):
total_items: int = pydantic.Field(
description="Total number of items.", examples=[42]
)
total_pages: int = pydantic.Field(
description="Total number of pages.", examples=[97]
)
current_page: int = pydantic.Field(
description="Current_page page number.", examples=[1]
)
page_size: int = pydantic.Field(
description="Number of items per page.", examples=[25]
)
class MyAgent(pydantic.BaseModel):
agent_id: str
agent_version: int
agent_name: str
last_edited: datetime.datetime
class MyAgentsResponse(pydantic.BaseModel):
agents: list[MyAgent]
pagination: Pagination
class StoreAgent(pydantic.BaseModel):
slug: str
agent_name: str
agent_image: str
creator: str
creator_avatar: str
sub_heading: str
description: str
runs: int
rating: float
class StoreAgentsResponse(pydantic.BaseModel):
agents: list[StoreAgent]
pagination: Pagination
class StoreAgentDetails(pydantic.BaseModel):
store_listing_version_id: str
slug: str
agent_name: str
agent_video: str
agent_image: list[str]
creator: str
creator_avatar: str
sub_heading: str
description: str
categories: list[str]
runs: int
rating: float
versions: list[str]
last_updated: datetime.datetime
class Creator(pydantic.BaseModel):
name: str
username: str
description: str
avatar_url: str
num_agents: int
agent_rating: float
agent_runs: int
class CreatorsResponse(pydantic.BaseModel):
creators: List[Creator]
pagination: Pagination
class CreatorDetails(pydantic.BaseModel):
name: str
username: str
description: str
links: list[str]
avatar_url: str
agent_rating: float
agent_runs: int
top_categories: list[str]
class Profile(pydantic.BaseModel):
name: str
username: str
description: str
links: list[str]
avatar_url: str
class StoreSubmission(pydantic.BaseModel):
agent_id: str
agent_version: int
name: str
sub_heading: str
slug: str
description: str
image_urls: list[str]
date_submitted: datetime.datetime
status: prisma.enums.SubmissionStatus
runs: int
rating: float
class StoreSubmissionsResponse(pydantic.BaseModel):
submissions: list[StoreSubmission]
pagination: Pagination
class StoreSubmissionRequest(pydantic.BaseModel):
agent_id: str
agent_version: int
slug: str
name: str
sub_heading: str
video_url: str | None = None
image_urls: list[str] = []
description: str = ""
categories: list[str] = []
class ProfileDetails(pydantic.BaseModel):
name: str
username: str
description: str
links: list[str]
avatar_url: str | None = None
class StoreReview(pydantic.BaseModel):
score: int
comments: str | None = None
class StoreReviewCreate(pydantic.BaseModel):
store_listing_version_id: str
score: int
comments: str | None = None

View File

@@ -0,0 +1,193 @@
import datetime
import prisma.enums
import backend.server.v2.store.model
def test_pagination():
pagination = backend.server.v2.store.model.Pagination(
total_items=100, total_pages=5, current_page=2, page_size=20
)
assert pagination.total_items == 100
assert pagination.total_pages == 5
assert pagination.current_page == 2
assert pagination.page_size == 20
def test_store_agent():
agent = backend.server.v2.store.model.StoreAgent(
slug="test-agent",
agent_name="Test Agent",
agent_image="test.jpg",
creator="creator1",
creator_avatar="avatar.jpg",
sub_heading="Test subheading",
description="Test description",
runs=50,
rating=4.5,
)
assert agent.slug == "test-agent"
assert agent.agent_name == "Test Agent"
assert agent.runs == 50
assert agent.rating == 4.5
def test_store_agents_response():
response = backend.server.v2.store.model.StoreAgentsResponse(
agents=[
backend.server.v2.store.model.StoreAgent(
slug="test-agent",
agent_name="Test Agent",
agent_image="test.jpg",
creator="creator1",
creator_avatar="avatar.jpg",
sub_heading="Test subheading",
description="Test description",
runs=50,
rating=4.5,
)
],
pagination=backend.server.v2.store.model.Pagination(
total_items=1, total_pages=1, current_page=1, page_size=20
),
)
assert len(response.agents) == 1
assert response.pagination.total_items == 1
def test_store_agent_details():
details = backend.server.v2.store.model.StoreAgentDetails(
store_listing_version_id="version123",
slug="test-agent",
agent_name="Test Agent",
agent_video="video.mp4",
agent_image=["image1.jpg", "image2.jpg"],
creator="creator1",
creator_avatar="avatar.jpg",
sub_heading="Test subheading",
description="Test description",
categories=["cat1", "cat2"],
runs=50,
rating=4.5,
versions=["1.0", "2.0"],
last_updated=datetime.datetime.now(),
)
assert details.slug == "test-agent"
assert len(details.agent_image) == 2
assert len(details.categories) == 2
assert len(details.versions) == 2
def test_creator():
creator = backend.server.v2.store.model.Creator(
agent_rating=4.8,
agent_runs=1000,
name="Test Creator",
username="creator1",
description="Test description",
avatar_url="avatar.jpg",
num_agents=5,
)
assert creator.name == "Test Creator"
assert creator.num_agents == 5
def test_creators_response():
response = backend.server.v2.store.model.CreatorsResponse(
creators=[
backend.server.v2.store.model.Creator(
agent_rating=4.8,
agent_runs=1000,
name="Test Creator",
username="creator1",
description="Test description",
avatar_url="avatar.jpg",
num_agents=5,
)
],
pagination=backend.server.v2.store.model.Pagination(
total_items=1, total_pages=1, current_page=1, page_size=20
),
)
assert len(response.creators) == 1
assert response.pagination.total_items == 1
def test_creator_details():
details = backend.server.v2.store.model.CreatorDetails(
name="Test Creator",
username="creator1",
description="Test description",
links=["link1.com", "link2.com"],
avatar_url="avatar.jpg",
agent_rating=4.8,
agent_runs=1000,
top_categories=["cat1", "cat2"],
)
assert details.name == "Test Creator"
assert len(details.links) == 2
assert details.agent_rating == 4.8
assert len(details.top_categories) == 2
def test_store_submission():
submission = backend.server.v2.store.model.StoreSubmission(
agent_id="agent123",
agent_version=1,
sub_heading="Test subheading",
name="Test Agent",
slug="test-agent",
description="Test description",
image_urls=["image1.jpg", "image2.jpg"],
date_submitted=datetime.datetime(2023, 1, 1),
status=prisma.enums.SubmissionStatus.PENDING,
runs=50,
rating=4.5,
)
assert submission.name == "Test Agent"
assert len(submission.image_urls) == 2
assert submission.status == prisma.enums.SubmissionStatus.PENDING
def test_store_submissions_response():
response = backend.server.v2.store.model.StoreSubmissionsResponse(
submissions=[
backend.server.v2.store.model.StoreSubmission(
agent_id="agent123",
agent_version=1,
sub_heading="Test subheading",
name="Test Agent",
slug="test-agent",
description="Test description",
image_urls=["image1.jpg"],
date_submitted=datetime.datetime(2023, 1, 1),
status=prisma.enums.SubmissionStatus.PENDING,
runs=50,
rating=4.5,
)
],
pagination=backend.server.v2.store.model.Pagination(
total_items=1, total_pages=1, current_page=1, page_size=20
),
)
assert len(response.submissions) == 1
assert response.pagination.total_items == 1
def test_store_submission_request():
request = backend.server.v2.store.model.StoreSubmissionRequest(
agent_id="agent123",
agent_version=1,
slug="test-agent",
name="Test Agent",
sub_heading="Test subheading",
video_url="video.mp4",
image_urls=["image1.jpg", "image2.jpg"],
description="Test description",
categories=["cat1", "cat2"],
)
assert request.agent_id == "agent123"
assert request.agent_version == 1
assert len(request.image_urls) == 2
assert len(request.categories) == 2

View File

@@ -0,0 +1,439 @@
import logging
import typing
import autogpt_libs.auth.depends
import autogpt_libs.auth.middleware
import fastapi
import fastapi.responses
import backend.server.v2.store.db
import backend.server.v2.store.media
import backend.server.v2.store.model
logger = logging.getLogger(__name__)
router = fastapi.APIRouter()
##############################################
############### Profile Endpoints ############
##############################################
@router.get("/profile", tags=["store", "private"])
async def get_profile(
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
]
) -> backend.server.v2.store.model.ProfileDetails:
"""
Get the profile details for the authenticated user.
"""
try:
profile = await backend.server.v2.store.db.get_user_profile(user_id)
return profile
except Exception:
logger.exception("Exception occurred whilst getting user profile")
raise
@router.post(
"/profile",
tags=["store", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
)
async def update_or_create_profile(
profile: backend.server.v2.store.model.Profile,
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
],
) -> backend.server.v2.store.model.CreatorDetails:
"""
Update the store profile for the authenticated user.
Args:
profile (Profile): The updated profile details
user_id (str): ID of the authenticated user
Returns:
CreatorDetails: The updated profile
Raises:
HTTPException: If there is an error updating the profile
"""
try:
updated_profile = await backend.server.v2.store.db.update_or_create_profile(
user_id=user_id, profile=profile
)
return updated_profile
except Exception:
logger.exception("Exception occurred whilst updating profile")
raise
##############################################
############### Agent Endpoints ##############
##############################################
@router.get("/agents", tags=["store", "public"])
async def get_agents(
featured: bool = False,
creator: str | None = None,
sorted_by: str | None = None,
search_query: str | None = None,
category: str | None = None,
page: int = 1,
page_size: int = 20,
) -> backend.server.v2.store.model.StoreAgentsResponse:
"""
Get a paginated list of agents from the store with optional filtering and sorting.
Args:
featured (bool, optional): Filter to only show featured agents. Defaults to False.
creator (str | None, optional): Filter agents by creator username. Defaults to None.
sorted_by (str | None, optional): Sort agents by "runs" or "rating". Defaults to None.
search_query (str | None, optional): Search agents by name, subheading and description. Defaults to None.
category (str | None, optional): Filter agents by category. Defaults to None.
page (int, optional): Page number for pagination. Defaults to 1.
page_size (int, optional): Number of agents per page. Defaults to 20.
Returns:
StoreAgentsResponse: Paginated list of agents matching the filters
Raises:
HTTPException: If page or page_size are less than 1
Used for:
- Home Page Featured Agents
- Home Page Top Agents
- Search Results
- Agent Details - Other Agents By Creator
- Agent Details - Similar Agents
- Creator Details - Agents By Creator
"""
if page < 1:
raise fastapi.HTTPException(
status_code=422, detail="Page must be greater than 0"
)
if page_size < 1:
raise fastapi.HTTPException(
status_code=422, detail="Page size must be greater than 0"
)
try:
agents = await backend.server.v2.store.db.get_store_agents(
featured=featured,
creator=creator,
sorted_by=sorted_by,
search_query=search_query,
category=category,
page=page,
page_size=page_size,
)
return agents
except Exception:
logger.exception("Exception occured whilst getting store agents")
raise
@router.get("/agents/{username}/{agent_name}", tags=["store", "public"])
async def get_agent(
username: str, agent_name: str
) -> backend.server.v2.store.model.StoreAgentDetails:
"""
This is only used on the AgentDetails Page
It returns the store listing agents details.
"""
try:
agent = await backend.server.v2.store.db.get_store_agent_details(
username=username, agent_name=agent_name
)
return agent
except Exception:
logger.exception("Exception occurred whilst getting store agent details")
raise
@router.post(
"/agents/{username}/{agent_name}/review",
tags=["store"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
)
async def create_review(
username: str,
agent_name: str,
review: backend.server.v2.store.model.StoreReviewCreate,
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
],
) -> backend.server.v2.store.model.StoreReview:
"""
Create a review for a store agent.
Args:
username: Creator's username
agent_name: Name/slug of the agent
review: Review details including score and optional comments
user_id: ID of authenticated user creating the review
Returns:
The created review
"""
try:
# Create the review
created_review = await backend.server.v2.store.db.create_store_review(
user_id=user_id,
store_listing_version_id=review.store_listing_version_id,
score=review.score,
comments=review.comments,
)
return created_review
except Exception:
logger.exception("Exception occurred whilst creating store review")
raise
##############################################
############# Creator Endpoints #############
##############################################
@router.get("/creators", tags=["store", "public"])
async def get_creators(
featured: bool = False,
search_query: str | None = None,
sorted_by: str | None = None,
page: int = 1,
page_size: int = 20,
) -> backend.server.v2.store.model.CreatorsResponse:
"""
This is needed for:
- Home Page Featured Creators
- Search Results Page
---
To support this functionality we need:
- featured: bool - to limit the list to just featured agents
- search_query: str - vector search based on the creators profile description.
- sorted_by: [agent_rating, agent_runs] -
"""
if page < 1:
raise fastapi.HTTPException(
status_code=422, detail="Page must be greater than 0"
)
if page_size < 1:
raise fastapi.HTTPException(
status_code=422, detail="Page size must be greater than 0"
)
try:
creators = await backend.server.v2.store.db.get_store_creators(
featured=featured,
search_query=search_query,
sorted_by=sorted_by,
page=page,
page_size=page_size,
)
return creators
except Exception:
logger.exception("Exception occurred whilst getting store creators")
raise
@router.get("/creator/{username}", tags=["store", "public"])
async def get_creator(username: str) -> backend.server.v2.store.model.CreatorDetails:
"""
Get the details of a creator
- Creator Details Page
"""
try:
creator = await backend.server.v2.store.db.get_store_creator_details(
username=username
)
return creator
except Exception:
logger.exception("Exception occurred whilst getting creator details")
raise
############################################
############# Store Submissions ###############
############################################
@router.get(
"/myagents",
tags=["store", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
)
async def get_my_agents(
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
]
) -> backend.server.v2.store.model.MyAgentsResponse:
try:
agents = await backend.server.v2.store.db.get_my_agents(user_id)
return agents
except Exception:
logger.exception("Exception occurred whilst getting my agents")
raise
@router.delete(
"/submissions/{submission_id}",
tags=["store", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
)
async def delete_submission(
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
],
submission_id: str,
) -> bool:
"""
Delete a store listing submission.
Args:
user_id (str): ID of the authenticated user
submission_id (str): ID of the submission to be deleted
Returns:
bool: True if the submission was successfully deleted, False otherwise
"""
try:
result = await backend.server.v2.store.db.delete_store_submission(
user_id=user_id,
submission_id=submission_id,
)
return result
except Exception:
logger.exception("Exception occurred whilst deleting store submission")
raise
@router.get(
"/submissions",
tags=["store", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
)
async def get_submissions(
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
],
page: int = 1,
page_size: int = 20,
) -> backend.server.v2.store.model.StoreSubmissionsResponse:
"""
Get a paginated list of store submissions for the authenticated user.
Args:
user_id (str): ID of the authenticated user
page (int, optional): Page number for pagination. Defaults to 1.
page_size (int, optional): Number of submissions per page. Defaults to 20.
Returns:
StoreListingsResponse: Paginated list of store submissions
Raises:
HTTPException: If page or page_size are less than 1
"""
if page < 1:
raise fastapi.HTTPException(
status_code=422, detail="Page must be greater than 0"
)
if page_size < 1:
raise fastapi.HTTPException(
status_code=422, detail="Page size must be greater than 0"
)
try:
listings = await backend.server.v2.store.db.get_store_submissions(
user_id=user_id,
page=page,
page_size=page_size,
)
return listings
except Exception:
logger.exception("Exception occurred whilst getting store submissions")
raise
@router.post(
"/submissions",
tags=["store", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
)
async def create_submission(
submission_request: backend.server.v2.store.model.StoreSubmissionRequest,
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
],
) -> backend.server.v2.store.model.StoreSubmission:
"""
Create a new store listing submission.
Args:
submission_request (StoreSubmissionRequest): The submission details
user_id (str): ID of the authenticated user submitting the listing
Returns:
StoreSubmission: The created store submission
Raises:
HTTPException: If there is an error creating the submission
"""
try:
submission = await backend.server.v2.store.db.create_store_submission(
user_id=user_id,
agent_id=submission_request.agent_id,
agent_version=submission_request.agent_version,
slug=submission_request.slug,
name=submission_request.name,
video_url=submission_request.video_url,
image_urls=submission_request.image_urls,
description=submission_request.description,
sub_heading=submission_request.sub_heading,
categories=submission_request.categories,
)
return submission
except Exception:
logger.exception("Exception occurred whilst creating store submission")
raise
@router.post(
"/submissions/media",
tags=["store", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
)
async def upload_submission_media(
file: fastapi.UploadFile,
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
],
) -> str:
"""
Upload media (images/videos) for a store listing submission.
Args:
file (UploadFile): The media file to upload
user_id (str): ID of the authenticated user uploading the media
Returns:
str: URL of the uploaded media file
Raises:
HTTPException: If there is an error uploading the media
"""
try:
media_url = await backend.server.v2.store.media.upload_media(
user_id=user_id, file=file
)
return media_url
except Exception:
logger.exception("Exception occurred whilst uploading submission media")
raise

View File

@@ -0,0 +1,551 @@
import datetime
import autogpt_libs.auth.depends
import autogpt_libs.auth.middleware
import fastapi
import fastapi.testclient
import prisma.enums
import pytest_mock
import backend.server.v2.store.model
import backend.server.v2.store.routes
app = fastapi.FastAPI()
app.include_router(backend.server.v2.store.routes.router)
client = fastapi.testclient.TestClient(app)
def override_auth_middleware():
"""Override auth middleware for testing"""
return {"sub": "test-user-id"}
def override_get_user_id():
"""Override get_user_id for testing"""
return "test-user-id"
app.dependency_overrides[autogpt_libs.auth.middleware.auth_middleware] = (
override_auth_middleware
)
app.dependency_overrides[autogpt_libs.auth.depends.get_user_id] = override_get_user_id
def test_get_agents_defaults(mocker: pytest_mock.MockFixture):
mocked_value = backend.server.v2.store.model.StoreAgentsResponse(
agents=[],
pagination=backend.server.v2.store.model.Pagination(
current_page=0,
total_items=0,
total_pages=0,
page_size=10,
),
)
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agents")
mock_db_call.return_value = mocked_value
response = client.get("/agents")
assert response.status_code == 200
data = backend.server.v2.store.model.StoreAgentsResponse.model_validate(
response.json()
)
assert data.pagination.total_pages == 0
assert data.agents == []
mock_db_call.assert_called_once_with(
featured=False,
creator=None,
sorted_by=None,
search_query=None,
category=None,
page=1,
page_size=20,
)
def test_get_agents_featured(mocker: pytest_mock.MockFixture):
mocked_value = backend.server.v2.store.model.StoreAgentsResponse(
agents=[
backend.server.v2.store.model.StoreAgent(
slug="featured-agent",
agent_name="Featured Agent",
agent_image="featured.jpg",
creator="creator1",
creator_avatar="avatar1.jpg",
sub_heading="Featured agent subheading",
description="Featured agent description",
runs=100,
rating=4.5,
)
],
pagination=backend.server.v2.store.model.Pagination(
current_page=1,
total_items=1,
total_pages=1,
page_size=20,
),
)
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agents")
mock_db_call.return_value = mocked_value
response = client.get("/agents?featured=true")
assert response.status_code == 200
data = backend.server.v2.store.model.StoreAgentsResponse.model_validate(
response.json()
)
assert len(data.agents) == 1
assert data.agents[0].slug == "featured-agent"
mock_db_call.assert_called_once_with(
featured=True,
creator=None,
sorted_by=None,
search_query=None,
category=None,
page=1,
page_size=20,
)
def test_get_agents_by_creator(mocker: pytest_mock.MockFixture):
mocked_value = backend.server.v2.store.model.StoreAgentsResponse(
agents=[
backend.server.v2.store.model.StoreAgent(
slug="creator-agent",
agent_name="Creator Agent",
agent_image="agent.jpg",
creator="specific-creator",
creator_avatar="avatar.jpg",
sub_heading="Creator agent subheading",
description="Creator agent description",
runs=50,
rating=4.0,
)
],
pagination=backend.server.v2.store.model.Pagination(
current_page=1,
total_items=1,
total_pages=1,
page_size=20,
),
)
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agents")
mock_db_call.return_value = mocked_value
response = client.get("/agents?creator=specific-creator")
assert response.status_code == 200
data = backend.server.v2.store.model.StoreAgentsResponse.model_validate(
response.json()
)
assert len(data.agents) == 1
assert data.agents[0].creator == "specific-creator"
mock_db_call.assert_called_once_with(
featured=False,
creator="specific-creator",
sorted_by=None,
search_query=None,
category=None,
page=1,
page_size=20,
)
def test_get_agents_sorted(mocker: pytest_mock.MockFixture):
mocked_value = backend.server.v2.store.model.StoreAgentsResponse(
agents=[
backend.server.v2.store.model.StoreAgent(
slug="top-agent",
agent_name="Top Agent",
agent_image="top.jpg",
creator="creator1",
creator_avatar="avatar1.jpg",
sub_heading="Top agent subheading",
description="Top agent description",
runs=1000,
rating=5.0,
)
],
pagination=backend.server.v2.store.model.Pagination(
current_page=1,
total_items=1,
total_pages=1,
page_size=20,
),
)
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agents")
mock_db_call.return_value = mocked_value
response = client.get("/agents?sorted_by=runs")
assert response.status_code == 200
data = backend.server.v2.store.model.StoreAgentsResponse.model_validate(
response.json()
)
assert len(data.agents) == 1
assert data.agents[0].runs == 1000
mock_db_call.assert_called_once_with(
featured=False,
creator=None,
sorted_by="runs",
search_query=None,
category=None,
page=1,
page_size=20,
)
def test_get_agents_search(mocker: pytest_mock.MockFixture):
mocked_value = backend.server.v2.store.model.StoreAgentsResponse(
agents=[
backend.server.v2.store.model.StoreAgent(
slug="search-agent",
agent_name="Search Agent",
agent_image="search.jpg",
creator="creator1",
creator_avatar="avatar1.jpg",
sub_heading="Search agent subheading",
description="Specific search term description",
runs=75,
rating=4.2,
)
],
pagination=backend.server.v2.store.model.Pagination(
current_page=1,
total_items=1,
total_pages=1,
page_size=20,
),
)
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agents")
mock_db_call.return_value = mocked_value
response = client.get("/agents?search_query=specific")
assert response.status_code == 200
data = backend.server.v2.store.model.StoreAgentsResponse.model_validate(
response.json()
)
assert len(data.agents) == 1
assert "specific" in data.agents[0].description.lower()
mock_db_call.assert_called_once_with(
featured=False,
creator=None,
sorted_by=None,
search_query="specific",
category=None,
page=1,
page_size=20,
)
def test_get_agents_category(mocker: pytest_mock.MockFixture):
mocked_value = backend.server.v2.store.model.StoreAgentsResponse(
agents=[
backend.server.v2.store.model.StoreAgent(
slug="category-agent",
agent_name="Category Agent",
agent_image="category.jpg",
creator="creator1",
creator_avatar="avatar1.jpg",
sub_heading="Category agent subheading",
description="Category agent description",
runs=60,
rating=4.1,
)
],
pagination=backend.server.v2.store.model.Pagination(
current_page=1,
total_items=1,
total_pages=1,
page_size=20,
),
)
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agents")
mock_db_call.return_value = mocked_value
response = client.get("/agents?category=test-category")
assert response.status_code == 200
data = backend.server.v2.store.model.StoreAgentsResponse.model_validate(
response.json()
)
assert len(data.agents) == 1
mock_db_call.assert_called_once_with(
featured=False,
creator=None,
sorted_by=None,
search_query=None,
category="test-category",
page=1,
page_size=20,
)
def test_get_agents_pagination(mocker: pytest_mock.MockFixture):
mocked_value = backend.server.v2.store.model.StoreAgentsResponse(
agents=[
backend.server.v2.store.model.StoreAgent(
slug=f"agent-{i}",
agent_name=f"Agent {i}",
agent_image=f"agent{i}.jpg",
creator="creator1",
creator_avatar="avatar1.jpg",
sub_heading=f"Agent {i} subheading",
description=f"Agent {i} description",
runs=i * 10,
rating=4.0,
)
for i in range(5)
],
pagination=backend.server.v2.store.model.Pagination(
current_page=2,
total_items=15,
total_pages=3,
page_size=5,
),
)
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agents")
mock_db_call.return_value = mocked_value
response = client.get("/agents?page=2&page_size=5")
assert response.status_code == 200
data = backend.server.v2.store.model.StoreAgentsResponse.model_validate(
response.json()
)
assert len(data.agents) == 5
assert data.pagination.current_page == 2
assert data.pagination.page_size == 5
mock_db_call.assert_called_once_with(
featured=False,
creator=None,
sorted_by=None,
search_query=None,
category=None,
page=2,
page_size=5,
)
def test_get_agents_malformed_request(mocker: pytest_mock.MockFixture):
# Test with invalid page number
response = client.get("/agents?page=-1")
assert response.status_code == 422
# Test with invalid page size
response = client.get("/agents?page_size=0")
assert response.status_code == 422
# Test with non-numeric values
response = client.get("/agents?page=abc&page_size=def")
assert response.status_code == 422
# Verify no DB calls were made
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agents")
mock_db_call.assert_not_called()
def test_get_agent_details(mocker: pytest_mock.MockFixture):
mocked_value = backend.server.v2.store.model.StoreAgentDetails(
store_listing_version_id="test-version-id",
slug="test-agent",
agent_name="Test Agent",
agent_video="video.mp4",
agent_image=["image1.jpg", "image2.jpg"],
creator="creator1",
creator_avatar="avatar1.jpg",
sub_heading="Test agent subheading",
description="Test agent description",
categories=["category1", "category2"],
runs=100,
rating=4.5,
versions=["1.0.0", "1.1.0"],
last_updated=datetime.datetime.now(),
)
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_agent_details")
mock_db_call.return_value = mocked_value
response = client.get("/agents/creator1/test-agent")
assert response.status_code == 200
data = backend.server.v2.store.model.StoreAgentDetails.model_validate(
response.json()
)
assert data.agent_name == "Test Agent"
assert data.creator == "creator1"
mock_db_call.assert_called_once_with(username="creator1", agent_name="test-agent")
def test_get_creators_defaults(mocker: pytest_mock.MockFixture):
mocked_value = backend.server.v2.store.model.CreatorsResponse(
creators=[],
pagination=backend.server.v2.store.model.Pagination(
current_page=0,
total_items=0,
total_pages=0,
page_size=10,
),
)
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_creators")
mock_db_call.return_value = mocked_value
response = client.get("/creators")
assert response.status_code == 200
data = backend.server.v2.store.model.CreatorsResponse.model_validate(
response.json()
)
assert data.pagination.total_pages == 0
assert data.creators == []
mock_db_call.assert_called_once_with(
featured=False, search_query=None, sorted_by=None, page=1, page_size=20
)
def test_get_creators_pagination(mocker: pytest_mock.MockFixture):
mocked_value = backend.server.v2.store.model.CreatorsResponse(
creators=[
backend.server.v2.store.model.Creator(
name=f"Creator {i}",
username=f"creator{i}",
description=f"Creator {i} description",
avatar_url=f"avatar{i}.jpg",
num_agents=1,
agent_rating=4.5,
agent_runs=100,
)
for i in range(5)
],
pagination=backend.server.v2.store.model.Pagination(
current_page=2,
total_items=15,
total_pages=3,
page_size=5,
),
)
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_creators")
mock_db_call.return_value = mocked_value
response = client.get("/creators?page=2&page_size=5")
assert response.status_code == 200
data = backend.server.v2.store.model.CreatorsResponse.model_validate(
response.json()
)
assert len(data.creators) == 5
assert data.pagination.current_page == 2
assert data.pagination.page_size == 5
mock_db_call.assert_called_once_with(
featured=False, search_query=None, sorted_by=None, page=2, page_size=5
)
def test_get_creators_malformed_request(mocker: pytest_mock.MockFixture):
# Test with invalid page number
response = client.get("/creators?page=-1")
assert response.status_code == 422
# Test with invalid page size
response = client.get("/creators?page_size=0")
assert response.status_code == 422
# Test with non-numeric values
response = client.get("/creators?page=abc&page_size=def")
assert response.status_code == 422
# Verify no DB calls were made
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_creators")
mock_db_call.assert_not_called()
def test_get_creator_details(mocker: pytest_mock.MockFixture):
mocked_value = backend.server.v2.store.model.CreatorDetails(
name="Test User",
username="creator1",
description="Test creator description",
links=["link1.com", "link2.com"],
avatar_url="avatar.jpg",
agent_rating=4.8,
agent_runs=1000,
top_categories=["category1", "category2"],
)
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_creator_details")
mock_db_call.return_value = mocked_value
response = client.get("/creator/creator1")
assert response.status_code == 200
data = backend.server.v2.store.model.CreatorDetails.model_validate(response.json())
assert data.username == "creator1"
assert data.name == "Test User"
mock_db_call.assert_called_once_with(username="creator1")
def test_get_submissions_success(mocker: pytest_mock.MockFixture):
mocked_value = backend.server.v2.store.model.StoreSubmissionsResponse(
submissions=[
backend.server.v2.store.model.StoreSubmission(
name="Test Agent",
description="Test agent description",
image_urls=["test.jpg"],
date_submitted=datetime.datetime.now(),
status=prisma.enums.SubmissionStatus.APPROVED,
runs=50,
rating=4.2,
agent_id="test-agent-id",
agent_version=1,
sub_heading="Test agent subheading",
slug="test-agent",
)
],
pagination=backend.server.v2.store.model.Pagination(
current_page=1,
total_items=1,
total_pages=1,
page_size=20,
),
)
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_submissions")
mock_db_call.return_value = mocked_value
response = client.get("/submissions")
assert response.status_code == 200
data = backend.server.v2.store.model.StoreSubmissionsResponse.model_validate(
response.json()
)
assert len(data.submissions) == 1
assert data.submissions[0].name == "Test Agent"
assert data.pagination.current_page == 1
mock_db_call.assert_called_once_with(user_id="test-user-id", page=1, page_size=20)
def test_get_submissions_pagination(mocker: pytest_mock.MockFixture):
mocked_value = backend.server.v2.store.model.StoreSubmissionsResponse(
submissions=[],
pagination=backend.server.v2.store.model.Pagination(
current_page=2,
total_items=10,
total_pages=2,
page_size=5,
),
)
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_submissions")
mock_db_call.return_value = mocked_value
response = client.get("/submissions?page=2&page_size=5")
assert response.status_code == 200
data = backend.server.v2.store.model.StoreSubmissionsResponse.model_validate(
response.json()
)
assert data.pagination.current_page == 2
assert data.pagination.page_size == 5
mock_db_call.assert_called_once_with(user_id="test-user-id", page=2, page_size=5)
def test_get_submissions_malformed_request(mocker: pytest_mock.MockFixture):
# Test with invalid page number
response = client.get("/submissions?page=-1")
assert response.status_code == 422
# Test with invalid page size
response = client.get("/submissions?page_size=0")
assert response.status_code == 422
# Test with non-numeric values
response = client.get("/submissions?page=abc&page_size=def")
assert response.status_code == 422
# Verify no DB calls were made
mock_db_call = mocker.patch("backend.server.v2.store.db.get_store_submissions")
mock_db_call.assert_not_called()

View File

@@ -148,6 +148,16 @@ class Config(UpdateTrackingModel["Config"], BaseSettings):
"This value is then used to generate redirect URLs for OAuth flows.",
)
media_gcs_bucket_name: str = Field(
default="",
description="The name of the Google Cloud Storage bucket for media files",
)
google_application_credentials: str = Field(
default="",
description="The path to the Google Cloud credentials JSON file",
)
@field_validator("platform_base_url", "frontend_base_url")
@classmethod
def validate_platform_base_url(cls, v: str, info: ValidationInfo) -> str:

View File

@@ -1,20 +1,31 @@
version: "3"
services:
postgres-test:
image: ankane/pgvector:latest
environment:
- POSTGRES_USER=agpt_user
- POSTGRES_PASSWORD=pass123
- POSTGRES_DB=agpt_local
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASS}
- POSTGRES_DB=${DB_NAME}
healthcheck:
test: pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB
interval: 10s
timeout: 5s
retries: 5
ports:
- "5433:5432"
- "${DB_PORT}:5432"
networks:
- app-network-test
redis-test:
image: redis:latest
command: redis-server --requirepass password
ports:
- "6379:6379"
networks:
- app-network-test
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
app-network-test:

View File

@@ -0,0 +1,228 @@
-- CreateEnum
CREATE TYPE "SubmissionStatus" AS ENUM ('DAFT', 'PENDING', 'APPROVED', 'REJECTED');
-- AlterTable
ALTER TABLE "AgentGraphExecution" ADD COLUMN "agentPresetId" TEXT;
-- AlterTable
ALTER TABLE "AgentNodeExecutionInputOutput" ADD COLUMN "agentPresetId" TEXT;
-- AlterTable
ALTER TABLE "AnalyticsMetrics" ALTER COLUMN "id" DROP DEFAULT;
-- AlterTable
ALTER TABLE "CreditTransaction" RENAME CONSTRAINT "UserBlockCredit_pkey" TO "CreditTransaction_pkey";
-- CreateTable
CREATE TABLE "AgentPreset" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"name" TEXT NOT NULL,
"description" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"userId" TEXT NOT NULL,
"agentId" TEXT NOT NULL,
"agentVersion" INTEGER NOT NULL,
CONSTRAINT "AgentPreset_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UserAgent" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" TEXT NOT NULL,
"agentId" TEXT NOT NULL,
"agentVersion" INTEGER NOT NULL,
"agentPresetId" TEXT,
"isFavorite" BOOLEAN NOT NULL DEFAULT false,
"isCreatedByUser" BOOLEAN NOT NULL DEFAULT false,
"isArchived" BOOLEAN NOT NULL DEFAULT false,
"isDeleted" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "UserAgent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Profile" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" TEXT,
"name" TEXT NOT NULL,
"username" TEXT NOT NULL,
"description" TEXT NOT NULL,
"links" TEXT[],
"avatarUrl" TEXT,
CONSTRAINT "Profile_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "StoreListing" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isDeleted" BOOLEAN NOT NULL DEFAULT false,
"isApproved" BOOLEAN NOT NULL DEFAULT false,
"agentId" TEXT NOT NULL,
"agentVersion" INTEGER NOT NULL,
"owningUserId" TEXT NOT NULL,
CONSTRAINT "StoreListing_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "StoreListingVersion" (
"id" TEXT NOT NULL,
"version" INTEGER NOT NULL DEFAULT 1,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"agentId" TEXT NOT NULL,
"agentVersion" INTEGER NOT NULL,
"slug" TEXT NOT NULL,
"name" TEXT NOT NULL,
"subHeading" TEXT NOT NULL,
"videoUrl" TEXT,
"imageUrls" TEXT[],
"description" TEXT NOT NULL,
"categories" TEXT[],
"isFeatured" BOOLEAN NOT NULL DEFAULT false,
"isDeleted" BOOLEAN NOT NULL DEFAULT false,
"isAvailable" BOOLEAN NOT NULL DEFAULT true,
"isApproved" BOOLEAN NOT NULL DEFAULT false,
"storeListingId" TEXT,
CONSTRAINT "StoreListingVersion_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "StoreListingReview" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"storeListingVersionId" TEXT NOT NULL,
"reviewByUserId" TEXT NOT NULL,
"score" INTEGER NOT NULL,
"comments" TEXT,
CONSTRAINT "StoreListingReview_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "StoreListingSubmission" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"storeListingId" TEXT NOT NULL,
"storeListingVersionId" TEXT NOT NULL,
"reviewerId" TEXT NOT NULL,
"Status" "SubmissionStatus" NOT NULL DEFAULT 'PENDING',
"reviewComments" TEXT,
CONSTRAINT "StoreListingSubmission_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "AgentPreset_userId_idx" ON "AgentPreset"("userId");
-- CreateIndex
CREATE INDEX "UserAgent_userId_idx" ON "UserAgent"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "Profile_username_key" ON "Profile"("username");
-- CreateIndex
CREATE INDEX "Profile_username_idx" ON "Profile"("username");
-- CreateIndex
CREATE INDEX "Profile_userId_idx" ON "Profile"("userId");
-- CreateIndex
CREATE INDEX "StoreListing_isApproved_idx" ON "StoreListing"("isApproved");
-- CreateIndex
CREATE INDEX "StoreListing_agentId_idx" ON "StoreListing"("agentId");
-- CreateIndex
CREATE INDEX "StoreListing_owningUserId_idx" ON "StoreListing"("owningUserId");
-- CreateIndex
CREATE INDEX "StoreListingVersion_agentId_agentVersion_isApproved_idx" ON "StoreListingVersion"("agentId", "agentVersion", "isApproved");
-- CreateIndex
CREATE UNIQUE INDEX "StoreListingVersion_agentId_agentVersion_key" ON "StoreListingVersion"("agentId", "agentVersion");
-- CreateIndex
CREATE INDEX "StoreListingReview_storeListingVersionId_idx" ON "StoreListingReview"("storeListingVersionId");
-- CreateIndex
CREATE UNIQUE INDEX "StoreListingReview_storeListingVersionId_reviewByUserId_key" ON "StoreListingReview"("storeListingVersionId", "reviewByUserId");
-- CreateIndex
CREATE INDEX "StoreListingSubmission_storeListingId_idx" ON "StoreListingSubmission"("storeListingId");
-- CreateIndex
CREATE INDEX "StoreListingSubmission_Status_idx" ON "StoreListingSubmission"("Status");
-- RenameForeignKey
ALTER TABLE "CreditTransaction" RENAME CONSTRAINT "UserBlockCredit_blockId_fkey" TO "CreditTransaction_blockId_fkey";
-- RenameForeignKey
ALTER TABLE "CreditTransaction" RENAME CONSTRAINT "UserBlockCredit_userId_fkey" TO "CreditTransaction_userId_fkey";
-- AddForeignKey
ALTER TABLE "AgentPreset" ADD CONSTRAINT "AgentPreset_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AgentPreset" ADD CONSTRAINT "AgentPreset_agentId_agentVersion_fkey" FOREIGN KEY ("agentId", "agentVersion") REFERENCES "AgentGraph"("id", "version") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserAgent" ADD CONSTRAINT "UserAgent_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserAgent" ADD CONSTRAINT "UserAgent_agentId_agentVersion_fkey" FOREIGN KEY ("agentId", "agentVersion") REFERENCES "AgentGraph"("id", "version") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UserAgent" ADD CONSTRAINT "UserAgent_agentPresetId_fkey" FOREIGN KEY ("agentPresetId") REFERENCES "AgentPreset"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AgentGraphExecution" ADD CONSTRAINT "AgentGraphExecution_agentPresetId_fkey" FOREIGN KEY ("agentPresetId") REFERENCES "AgentPreset"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AgentNodeExecutionInputOutput" ADD CONSTRAINT "AgentNodeExecutionInputOutput_agentPresetId_fkey" FOREIGN KEY ("agentPresetId") REFERENCES "AgentPreset"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Profile" ADD CONSTRAINT "Profile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StoreListing" ADD CONSTRAINT "StoreListing_agentId_agentVersion_fkey" FOREIGN KEY ("agentId", "agentVersion") REFERENCES "AgentGraph"("id", "version") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StoreListing" ADD CONSTRAINT "StoreListing_owningUserId_fkey" FOREIGN KEY ("owningUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StoreListingVersion" ADD CONSTRAINT "StoreListingVersion_agentId_agentVersion_fkey" FOREIGN KEY ("agentId", "agentVersion") REFERENCES "AgentGraph"("id", "version") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StoreListingVersion" ADD CONSTRAINT "StoreListingVersion_storeListingId_fkey" FOREIGN KEY ("storeListingId") REFERENCES "StoreListing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StoreListingReview" ADD CONSTRAINT "StoreListingReview_storeListingVersionId_fkey" FOREIGN KEY ("storeListingVersionId") REFERENCES "StoreListingVersion"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StoreListingReview" ADD CONSTRAINT "StoreListingReview_reviewByUserId_fkey" FOREIGN KEY ("reviewByUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StoreListingSubmission" ADD CONSTRAINT "StoreListingSubmission_storeListingId_fkey" FOREIGN KEY ("storeListingId") REFERENCES "StoreListing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StoreListingSubmission" ADD CONSTRAINT "StoreListingSubmission_storeListingVersionId_fkey" FOREIGN KEY ("storeListingVersionId") REFERENCES "StoreListingVersion"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StoreListingSubmission" ADD CONSTRAINT "StoreListingSubmission_reviewerId_fkey" FOREIGN KEY ("reviewerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- RenameIndex
ALTER INDEX "UserBlockCredit_userId_createdAt_idx" RENAME TO "CreditTransaction_userId_createdAt_idx";

View File

@@ -0,0 +1,118 @@
BEGIN;
CREATE VIEW "StoreAgent" AS
WITH ReviewStats AS (
SELECT sl."id" AS "storeListingId",
COUNT(sr.id) AS review_count,
AVG(CAST(sr.score AS DECIMAL)) AS avg_rating
FROM "StoreListing" sl
JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl."id"
JOIN "StoreListingReview" sr ON sr."storeListingVersionId" = slv.id
WHERE sl."isDeleted" = FALSE
GROUP BY sl."id"
),
AgentRuns AS (
SELECT "agentGraphId", COUNT(*) AS run_count
FROM "AgentGraphExecution"
GROUP BY "agentGraphId"
)
SELECT
sl.id AS listing_id,
slv.id AS "storeListingVersionId",
slv."createdAt" AS updated_at,
slv.slug,
a.name AS agent_name,
slv."videoUrl" AS agent_video,
COALESCE(slv."imageUrls", ARRAY[]::TEXT[]) AS agent_image,
slv."isFeatured" AS featured,
p.username AS creator_username,
p."avatarUrl" AS creator_avatar,
slv."subHeading" AS sub_heading,
slv.description,
slv.categories,
COALESCE(ar.run_count, 0) AS runs,
CAST(COALESCE(rs.avg_rating, 0.0) AS DOUBLE PRECISION) AS rating,
ARRAY_AGG(DISTINCT CAST(slv.version AS TEXT)) AS versions
FROM "StoreListing" sl
JOIN "AgentGraph" a ON sl."agentId" = a.id AND sl."agentVersion" = a."version"
LEFT JOIN "Profile" p ON sl."owningUserId" = p."userId"
LEFT JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl.id
LEFT JOIN ReviewStats rs ON sl.id = rs."storeListingId"
LEFT JOIN AgentRuns ar ON a.id = ar."agentGraphId"
WHERE sl."isDeleted" = FALSE
AND sl."isApproved" = TRUE
GROUP BY sl.id, slv.id, slv.slug, slv."createdAt", a.name, slv."videoUrl", slv."imageUrls", slv."isFeatured",
p.username, p."avatarUrl", slv."subHeading", slv.description, slv.categories,
ar.run_count, rs.avg_rating;
CREATE VIEW "Creator" AS
WITH AgentStats AS (
SELECT
p.username,
COUNT(DISTINCT sl.id) as num_agents,
AVG(CAST(COALESCE(sr.score, 0) AS DECIMAL)) as agent_rating,
SUM(COALESCE(age.run_count, 0)) as agent_runs
FROM "Profile" p
LEFT JOIN "StoreListing" sl ON sl."owningUserId" = p."userId"
LEFT JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl.id
LEFT JOIN "StoreListingReview" sr ON sr."storeListingVersionId" = slv.id
LEFT JOIN (
SELECT "agentGraphId", COUNT(*) as run_count
FROM "AgentGraphExecution"
GROUP BY "agentGraphId"
) age ON age."agentGraphId" = sl."agentId"
WHERE sl."isDeleted" = FALSE AND sl."isApproved" = TRUE
GROUP BY p.username
)
SELECT
p.username,
p.name,
p."avatarUrl" as avatar_url,
p.description,
ARRAY_AGG(DISTINCT c) FILTER (WHERE c IS NOT NULL) as top_categories,
p.links,
COALESCE(ast.num_agents, 0) as num_agents,
COALESCE(ast.agent_rating, 0.0) as agent_rating,
COALESCE(ast.agent_runs, 0) as agent_runs
FROM "Profile" p
LEFT JOIN AgentStats ast ON ast.username = p.username
LEFT JOIN LATERAL (
SELECT UNNEST(slv.categories) as c
FROM "StoreListing" sl
JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl.id
WHERE sl."owningUserId" = p."userId"
AND sl."isDeleted" = FALSE
AND sl."isApproved" = TRUE
) cats ON true
GROUP BY p.username, p.name, p."avatarUrl", p.description, p.links,
ast.num_agents, ast.agent_rating, ast.agent_runs;
CREATE VIEW "StoreSubmission" AS
SELECT
sl.id as listing_id,
sl."owningUserId" as user_id,
slv."agentId" as agent_id,
slv."version" as agent_version,
slv.slug,
slv.name,
slv."subHeading" as sub_heading,
slv.description,
slv."imageUrls" as image_urls,
slv."createdAt" as date_submitted,
COALESCE(sls."Status", 'PENDING') as status,
COALESCE(ar.run_count, 0) as runs,
CAST(COALESCE(AVG(CAST(sr.score AS DECIMAL)), 0.0) AS DOUBLE PRECISION) as rating
FROM "StoreListing" sl
JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl.id
LEFT JOIN "StoreListingSubmission" sls ON sls."storeListingId" = sl.id
LEFT JOIN "StoreListingReview" sr ON sr."storeListingVersionId" = slv.id
LEFT JOIN (
SELECT "agentGraphId", COUNT(*) as run_count
FROM "AgentGraphExecution"
GROUP BY "agentGraphId"
) ar ON ar."agentGraphId" = slv."agentId"
WHERE sl."isDeleted" = FALSE
GROUP BY sl.id, sl."owningUserId", slv."agentId", slv."version", slv.slug, slv.name, slv."subHeading",
slv.description, slv."imageUrls", slv."createdAt", sls."Status", ar.run_count;
COMMIT;

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ packages = [{ include = "backend" }]
[tool.poetry.dependencies]
python = "^3.10"
python = ">=3.10,<3.13"
aio-pika = "^9.5.0"
anthropic = "^0.39.0"
apscheduler = "^3.11.0"
@@ -49,8 +49,10 @@ googlemaps = "^4.10.0"
replicate = "^1.0.4"
pinecone = "^5.3.1"
cryptography = "^43.0.3"
python-multipart = "^0.0.17"
sqlalchemy = "^2.0.36"
psycopg2-binary = "^2.9.10"
google-cloud-storage = "^2.18.2"
launchdarkly-server-sdk = "^9.8.0"
[tool.poetry.group.dev.dependencies]
poethepoet = "^0.31.0"
@@ -62,6 +64,8 @@ pyright = "^1.1.389"
isort = "^5.13.2"
black = "^24.10.0"
aiohappyeyeballs = "^2.4.3"
pytest-mock = "^3.14.0"
faker = "^30.8.2"
[build-system]
requires = ["poetry-core"]

View File

@@ -16,9 +16,9 @@ def wait_for_postgres(max_retries=5, delay=5):
"postgres-test",
"pg_isready",
"-U",
"agpt_user",
"postgres",
"-d",
"agpt_local",
"postgres",
],
check=True,
capture_output=True,

View File

@@ -8,6 +8,7 @@ generator client {
provider = "prisma-client-py"
recursive_type_depth = 5
interface = "asyncio"
previewFeatures = ["views"]
}
// User model to mirror Auth provider users
@@ -24,11 +25,19 @@ model User {
// Relations
AgentGraphs AgentGraph[]
AgentGraphExecutions AgentGraphExecution[]
IntegrationWebhooks IntegrationWebhook[]
AnalyticsDetails AnalyticsDetails[]
AnalyticsMetrics AnalyticsMetrics[]
CreditTransaction CreditTransaction[]
APIKeys APIKey[]
AgentPreset AgentPreset[]
UserAgent UserAgent[]
Profile Profile[]
StoreListing StoreListing[]
StoreListingReview StoreListingReview[]
StoreListingSubmission StoreListingSubmission[]
APIKeys APIKey[]
IntegrationWebhooks IntegrationWebhook[]
@@index([id])
@@index([email])
@@ -48,15 +57,89 @@ model AgentGraph {
// Link to User model
userId String
// FIX: Do not cascade delete the agent when the user is deleted
// This allows us to delete user data with deleting the agent which maybe in use by other users
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
AgentNodes AgentNode[]
AgentGraphExecution AgentGraphExecution[]
AgentPreset AgentPreset[]
UserAgent UserAgent[]
StoreListing StoreListing[]
StoreListingVersion StoreListingVersion?
@@id(name: "graphVersionId", [id, version])
@@index([userId, isActive])
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
//////////////// USER SPECIFIC DATA ////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
// An AgentPrest is an Agent + User Configuration of that agent.
// For example, if someone has created a weather agent and they want to set it up to
// Inform them of extreme weather warnings in Texas, the agent with the configuration to set it to
// monitor texas, along with the cron setup or webhook tiggers, is an AgentPreset
model AgentPreset {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
name String
description String
// For agents that can be triggered by webhooks or cronjob
// This bool allows us to disable a configured agent without deleting it
isActive Boolean @default(true)
userId String
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
agentId String
agentVersion Int
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade)
InputPresets AgentNodeExecutionInputOutput[] @relation("AgentPresetsInputData")
UserAgents UserAgent[]
AgentExecution AgentGraphExecution[]
@@index([userId])
}
// For the library page
// It is a user controlled list of agents, that they will see in there library
model UserAgent {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
userId String
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
agentId String
agentVersion Int
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version])
agentPresetId String?
AgentPreset AgentPreset? @relation(fields: [agentPresetId], references: [id])
isFavorite Boolean @default(false)
isCreatedByUser Boolean @default(false)
isArchived Boolean @default(false)
isDeleted Boolean @default(false)
@@index([userId])
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
//////// AGENT DEFINITION AND EXECUTION TABLES ////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
// This model describes a single node in the Agent Graph/Flow (Multi Agent System).
model AgentNode {
id String @id @default(uuid())
@@ -155,7 +238,9 @@ model AgentGraphExecution {
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
stats String? // JSON serialized object
stats String? // JSON serialized object
AgentPreset AgentPreset? @relation(fields: [agentPresetId], references: [id])
agentPresetId String?
@@index([agentGraphId, agentGraphVersion])
@@index([userId])
@@ -202,6 +287,9 @@ model AgentNodeExecutionInputOutput {
referencedByOutputExecId String?
ReferencedByOutputExec AgentNodeExecution? @relation("AgentNodeExecutionOutput", fields: [referencedByOutputExecId], references: [id], onDelete: Cascade)
agentPresetId String?
AgentPreset AgentPreset? @relation("AgentPresetsInputData", fields: [agentPresetId], references: [id])
// Input and Output pin names are unique for each AgentNodeExecution.
@@unique([referencedByInputExecId, referencedByOutputExecId, name])
@@index([referencedByOutputExecId])
@@ -256,8 +344,13 @@ model AnalyticsDetails {
@@index([type])
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////// METRICS TRACKING TABLES ////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
model AnalyticsMetrics {
id String @id @default(dbgenerated("gen_random_uuid()"))
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -281,6 +374,11 @@ enum CreditTransactionType {
USAGE
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
//////// ACCOUNTING AND CREDIT SYSTEM TABLES //////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
model CreditTransaction {
transactionKey String @default(uuid())
createdAt DateTime @default(now())
@@ -301,6 +399,205 @@ model CreditTransaction {
@@index([userId, createdAt])
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////// Store TABLES ///////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
model Profile {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
// Only 1 of user or group can be set.
// The user this profile belongs to, if any.
userId String?
User User? @relation(fields: [userId], references: [id], onDelete: Cascade)
name String
username String @unique
description String
links String[]
avatarUrl String?
@@index([username])
@@index([userId])
}
view Creator {
username String @unique
name String
avatar_url String
description String
top_categories String[]
links String[]
num_agents Int
agent_rating Float
agent_runs Int
}
view StoreAgent {
listing_id String @id
storeListingVersionId String
updated_at DateTime
slug String
agent_name String
agent_video String?
agent_image String[]
featured Boolean @default(false)
creator_username String
creator_avatar String
sub_heading String
description String
categories String[]
runs Int
rating Float
versions String[]
@@unique([creator_username, slug])
@@index([creator_username])
@@index([featured])
@@index([categories])
@@index([storeListingVersionId])
}
view StoreSubmission {
listing_id String @id
user_id String
slug String
name String
sub_heading String
description String
image_urls String[]
date_submitted DateTime
status SubmissionStatus
runs Int
rating Float
agent_id String
agent_version Int
@@index([user_id])
}
model StoreListing {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
isDeleted Boolean @default(false)
// Not needed but makes lookups faster
isApproved Boolean @default(false)
// The agent link here is only so we can do lookup on agentId, for the listing the StoreListingVersion is used.
agentId String
agentVersion Int
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version], onDelete: Cascade)
owningUserId String
OwningUser User @relation(fields: [owningUserId], references: [id])
StoreListingVersions StoreListingVersion[]
StoreListingSubmission StoreListingSubmission[]
@@index([isApproved])
@@index([agentId])
@@index([owningUserId])
}
model StoreListingVersion {
id String @id @default(uuid())
version Int @default(1)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
// The agent and version to be listed on the store
agentId String
agentVersion Int
Agent AgentGraph @relation(fields: [agentId, agentVersion], references: [id, version])
// The detials for this version of the agent, this allows the author to update the details of the agent,
// But still allow using old versions of the agent with there original details.
// TODO: Create a database view that shows only the latest version of each store listing.
slug String
name String
subHeading String
videoUrl String?
imageUrls String[]
description String
categories String[]
isFeatured Boolean @default(false)
isDeleted Boolean @default(false)
// Old versions can be made unavailable by the author if desired
isAvailable Boolean @default(true)
// Not needed but makes lookups faster
isApproved Boolean @default(false)
StoreListing StoreListing? @relation(fields: [storeListingId], references: [id], onDelete: Cascade)
storeListingId String?
StoreListingSubmission StoreListingSubmission[]
// Reviews are on a specific version, but then aggregated up to the listing.
// This allows us to provide a review filter to current version of the agent.
StoreListingReview StoreListingReview[]
@@unique([agentId, agentVersion])
@@index([agentId, agentVersion, isApproved])
}
model StoreListingReview {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
storeListingVersionId String
StoreListingVersion StoreListingVersion @relation(fields: [storeListingVersionId], references: [id], onDelete: Cascade)
reviewByUserId String
ReviewByUser User @relation(fields: [reviewByUserId], references: [id])
score Int
comments String?
@@unique([storeListingVersionId, reviewByUserId])
@@index([storeListingVersionId])
}
enum SubmissionStatus {
DAFT
PENDING
APPROVED
REJECTED
}
model StoreListingSubmission {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
storeListingId String
StoreListing StoreListing @relation(fields: [storeListingId], references: [id], onDelete: Cascade)
storeListingVersionId String
StoreListingVersion StoreListingVersion @relation(fields: [storeListingVersionId], references: [id], onDelete: Cascade)
reviewerId String
Reviewer User @relation(fields: [reviewerId], references: [id])
Status SubmissionStatus @default(PENDING)
reviewComments String?
@@index([storeListingId])
@@index([Status])
}
enum APIKeyPermission {
EXECUTE_GRAPH // Can execute agent graphs
READ_GRAPH // Can get graph versions and details
@@ -338,4 +635,4 @@ enum APIKeyStatus {
ACTIVE
REVOKED
SUSPENDED
}
}

View File

@@ -0,0 +1,457 @@
import asyncio
import random
from datetime import datetime
import prisma.enums
from faker import Faker
from prisma import Prisma
faker = Faker()
# Constants for data generation limits
# Base entities
NUM_USERS = 100 # Creates 100 user records
NUM_AGENT_BLOCKS = 100 # Creates 100 agent block templates
# Per-user entities
MIN_GRAPHS_PER_USER = 1 # Each user will have between 1-5 graphs
MAX_GRAPHS_PER_USER = 5 # Total graphs: 500-2500 (NUM_USERS * MIN/MAX_GRAPHS)
# Per-graph entities
MIN_NODES_PER_GRAPH = 2 # Each graph will have between 2-5 nodes
MAX_NODES_PER_GRAPH = (
5 # Total nodes: 1000-2500 (GRAPHS_PER_USER * NUM_USERS * MIN/MAX_NODES)
)
# Additional per-user entities
MIN_PRESETS_PER_USER = 1 # Each user will have between 1-2 presets
MAX_PRESETS_PER_USER = 5 # Total presets: 500-2500 (NUM_USERS * MIN/MAX_PRESETS)
MIN_AGENTS_PER_USER = 1 # Each user will have between 1-2 agents
MAX_AGENTS_PER_USER = 10 # Total agents: 500-5000 (NUM_USERS * MIN/MAX_AGENTS)
# Execution and review records
MIN_EXECUTIONS_PER_GRAPH = 1 # Each graph will have between 1-5 execution records
MAX_EXECUTIONS_PER_GRAPH = (
20 # Total executions: 1000-5000 (TOTAL_GRAPHS * MIN/MAX_EXECUTIONS)
)
MIN_REVIEWS_PER_VERSION = 1 # Each version will have between 1-3 reviews
MAX_REVIEWS_PER_VERSION = 5 # Total reviews depends on number of versions created
def get_image():
url = faker.image_url()
while "placekitten.com" in url:
url = faker.image_url()
return url
async def main():
db = Prisma()
await db.connect()
# Insert Users
print(f"Inserting {NUM_USERS} users")
users = []
for _ in range(NUM_USERS):
user = await db.user.create(
data={
"id": str(faker.uuid4()),
"email": faker.unique.email(),
"name": faker.name(),
"metadata": prisma.Json({}),
"integrations": "",
}
)
users.append(user)
# Insert AgentBlocks
agent_blocks = []
print(f"Inserting {NUM_AGENT_BLOCKS} agent blocks")
for _ in range(NUM_AGENT_BLOCKS):
block = await db.agentblock.create(
data={
"name": f"{faker.word()}_{str(faker.uuid4())[:8]}",
"inputSchema": "{}",
"outputSchema": "{}",
}
)
agent_blocks.append(block)
# Insert AgentGraphs
agent_graphs = []
print(f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER} agent graphs")
for user in users:
for _ in range(
random.randint(MIN_GRAPHS_PER_USER, MAX_GRAPHS_PER_USER)
): # Adjust the range to create more graphs per user if desired
graph = await db.agentgraph.create(
data={
"name": faker.sentence(nb_words=3),
"description": faker.text(max_nb_chars=200),
"userId": user.id,
"isActive": True,
"isTemplate": False,
}
)
agent_graphs.append(graph)
# Insert AgentNodes
agent_nodes = []
print(
f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER * MAX_NODES_PER_GRAPH} agent nodes"
)
for graph in agent_graphs:
num_nodes = random.randint(MIN_NODES_PER_GRAPH, MAX_NODES_PER_GRAPH)
for _ in range(num_nodes): # Create 5 AgentNodes per graph
block = random.choice(agent_blocks)
node = await db.agentnode.create(
data={
"agentBlockId": block.id,
"agentGraphId": graph.id,
"agentGraphVersion": graph.version,
"constantInput": "{}",
"metadata": "{}",
}
)
agent_nodes.append(node)
# Insert AgentPresets
agent_presets = []
print(f"Inserting {NUM_USERS * MAX_PRESETS_PER_USER} agent presets")
for user in users:
num_presets = random.randint(MIN_PRESETS_PER_USER, MAX_PRESETS_PER_USER)
for _ in range(num_presets): # Create 1 AgentPreset per user
graph = random.choice(agent_graphs)
preset = await db.agentpreset.create(
data={
"name": faker.sentence(nb_words=3),
"description": faker.text(max_nb_chars=200),
"userId": user.id,
"agentId": graph.id,
"agentVersion": graph.version,
"isActive": True,
}
)
agent_presets.append(preset)
# Insert UserAgents
user_agents = []
print(f"Inserting {NUM_USERS * MAX_AGENTS_PER_USER} user agents")
for user in users:
num_agents = random.randint(MIN_AGENTS_PER_USER, MAX_AGENTS_PER_USER)
for _ in range(num_agents): # Create 1 UserAgent per user
graph = random.choice(agent_graphs)
preset = random.choice(agent_presets)
user_agent = await db.useragent.create(
data={
"userId": user.id,
"agentId": graph.id,
"agentVersion": graph.version,
"agentPresetId": preset.id,
"isFavorite": random.choice([True, False]),
"isCreatedByUser": random.choice([True, False]),
"isArchived": random.choice([True, False]),
"isDeleted": random.choice([True, False]),
}
)
user_agents.append(user_agent)
# Insert AgentGraphExecutions
# Insert AgentGraphExecutions
agent_graph_executions = []
print(
f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER * MAX_EXECUTIONS_PER_GRAPH} agent graph executions"
)
graph_execution_data = []
for graph in agent_graphs:
user = random.choice(users)
num_executions = random.randint(
MIN_EXECUTIONS_PER_GRAPH, MAX_EXECUTIONS_PER_GRAPH
)
for _ in range(num_executions):
matching_presets = [p for p in agent_presets if p.agentId == graph.id]
preset = (
random.choice(matching_presets)
if matching_presets and random.random() < 0.5
else None
)
graph_execution_data.append(
{
"agentGraphId": graph.id,
"agentGraphVersion": graph.version,
"userId": user.id,
"executionStatus": prisma.enums.AgentExecutionStatus.COMPLETED,
"startedAt": faker.date_time_this_year(),
"agentPresetId": preset.id if preset else None,
}
)
agent_graph_executions = await db.agentgraphexecution.create_many(
data=graph_execution_data
)
# Need to fetch the created records since create_many doesn't return them
agent_graph_executions = await db.agentgraphexecution.find_many()
# Insert AgentNodeExecutions
print(
f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER * MAX_EXECUTIONS_PER_GRAPH} agent node executions"
)
node_execution_data = []
for execution in agent_graph_executions:
nodes = [
node for node in agent_nodes if node.agentGraphId == execution.agentGraphId
]
for node in nodes:
node_execution_data.append(
{
"agentGraphExecutionId": execution.id,
"agentNodeId": node.id,
"executionStatus": prisma.enums.AgentExecutionStatus.COMPLETED,
"addedTime": datetime.now(),
}
)
agent_node_executions = await db.agentnodeexecution.create_many(
data=node_execution_data
)
# Need to fetch the created records since create_many doesn't return them
agent_node_executions = await db.agentnodeexecution.find_many()
# Insert AgentNodeExecutionInputOutput
print(
f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER * MAX_EXECUTIONS_PER_GRAPH} agent node execution input/outputs"
)
input_output_data = []
for node_execution in agent_node_executions:
# Input data
input_output_data.append(
{
"name": "input1",
"data": "{}",
"time": datetime.now(),
"referencedByInputExecId": node_execution.id,
}
)
# Output data
input_output_data.append(
{
"name": "output1",
"data": "{}",
"time": datetime.now(),
"referencedByOutputExecId": node_execution.id,
}
)
await db.agentnodeexecutioninputoutput.create_many(data=input_output_data)
# Insert AgentGraphExecutionSchedules
agent_graph_execution_schedules = []
print(
f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER} agent graph execution schedules"
)
for graph in agent_graphs:
user = random.choice(users)
schedule = await db.agentgraphexecutionschedule.create(
data={
"id": str(faker.uuid4()),
"agentGraphId": graph.id,
"agentGraphVersion": graph.version,
"schedule": "* * * * *",
"isEnabled": True,
"inputData": "{}",
"userId": user.id,
"lastUpdated": datetime.now(),
}
)
agent_graph_execution_schedules.append(schedule)
# Insert AgentNodeLinks
print(f"Inserting {NUM_USERS * MAX_GRAPHS_PER_USER} agent node links")
for graph in agent_graphs:
nodes = [node for node in agent_nodes if node.agentGraphId == graph.id]
if len(nodes) >= 2:
source_node = nodes[0]
sink_node = nodes[1]
await db.agentnodelink.create(
data={
"agentNodeSourceId": source_node.id,
"sourceName": "output1",
"agentNodeSinkId": sink_node.id,
"sinkName": "input1",
"isStatic": False,
}
)
# Insert AnalyticsDetails
print(f"Inserting {NUM_USERS} analytics details")
for user in users:
for _ in range(1):
await db.analyticsdetails.create(
data={
"userId": user.id,
"type": faker.word(),
"data": prisma.Json({}),
"dataIndex": faker.word(),
}
)
# Insert AnalyticsMetrics
print(f"Inserting {NUM_USERS} analytics metrics")
for user in users:
for _ in range(1):
await db.analyticsmetrics.create(
data={
"userId": user.id,
"analyticMetric": faker.word(),
"value": random.uniform(0, 100),
"dataString": faker.word(),
}
)
# Insert UserBlockCredit
print(f"Inserting {NUM_USERS} user block credits")
for user in users:
for _ in range(1):
block = random.choice(agent_blocks)
await db.userblockcredit.create(
data={
"transactionKey": str(faker.uuid4()),
"userId": user.id,
"blockId": block.id,
"amount": random.randint(1, 100),
"type": (
prisma.enums.UserBlockCreditType.TOP_UP
if random.random() < 0.5
else prisma.enums.UserBlockCreditType.USAGE
),
"metadata": prisma.Json({}),
}
)
# Insert Profiles
profiles = []
print(f"Inserting {NUM_USERS} profiles")
for user in users:
profile = await db.profile.create(
data={
"userId": user.id,
"name": user.name or faker.name(),
"username": faker.unique.user_name(),
"description": faker.text(),
"links": [faker.url() for _ in range(3)],
"avatarUrl": get_image(),
}
)
profiles.append(profile)
# Insert StoreListings
store_listings = []
print(f"Inserting {NUM_USERS} store listings")
for graph in agent_graphs:
user = random.choice(users)
listing = await db.storelisting.create(
data={
"agentId": graph.id,
"agentVersion": graph.version,
"owningUserId": user.id,
"isApproved": random.choice([True, False]),
}
)
store_listings.append(listing)
# Insert StoreListingVersions
store_listing_versions = []
print(f"Inserting {NUM_USERS} store listing versions")
for listing in store_listings:
graph = [g for g in agent_graphs if g.id == listing.agentId][0]
version = await db.storelistingversion.create(
data={
"agentId": graph.id,
"agentVersion": graph.version,
"slug": faker.slug(),
"name": graph.name or faker.sentence(nb_words=3),
"subHeading": faker.sentence(),
"videoUrl": faker.url(),
"imageUrls": [get_image() for _ in range(3)],
"description": faker.text(),
"categories": [faker.word() for _ in range(3)],
"isFeatured": random.choice([True, False]),
"isAvailable": True,
"isApproved": random.choice([True, False]),
"storeListingId": listing.id,
}
)
store_listing_versions.append(version)
# Insert StoreListingReviews
print(f"Inserting {NUM_USERS * MAX_REVIEWS_PER_VERSION} store listing reviews")
for version in store_listing_versions:
# Create a copy of users list and shuffle it to avoid duplicates
available_reviewers = users.copy()
random.shuffle(available_reviewers)
# Limit number of reviews to available unique reviewers
num_reviews = min(
random.randint(MIN_REVIEWS_PER_VERSION, MAX_REVIEWS_PER_VERSION),
len(available_reviewers),
)
# Take only the first num_reviews reviewers
for reviewer in available_reviewers[:num_reviews]:
await db.storelistingreview.create(
data={
"storeListingVersionId": version.id,
"reviewByUserId": reviewer.id,
"score": random.randint(1, 5),
"comments": faker.text(),
}
)
# Insert StoreListingSubmissions
print(f"Inserting {NUM_USERS} store listing submissions")
for listing in store_listings:
version = random.choice(store_listing_versions)
reviewer = random.choice(users)
status: prisma.enums.SubmissionStatus = random.choice(
[
prisma.enums.SubmissionStatus.PENDING,
prisma.enums.SubmissionStatus.APPROVED,
prisma.enums.SubmissionStatus.REJECTED,
]
)
await db.storelistingsubmission.create(
data={
"storeListingId": listing.id,
"storeListingVersionId": version.id,
"reviewerId": reviewer.id,
"Status": status,
"reviewComments": faker.text(),
}
)
# Insert APIKeys
print(f"Inserting {NUM_USERS} api keys")
for user in users:
await db.apikey.create(
data={
"name": faker.word(),
"prefix": str(faker.uuid4())[:8],
"postfix": str(faker.uuid4())[-8:],
"key": str(faker.sha256()),
"status": prisma.enums.APIKeyStatus.ACTIVE,
"permissions": [
prisma.enums.APIKeyPermission.EXECUTE_GRAPH,
prisma.enums.APIKeyPermission.READ_GRAPH,
],
"description": faker.text(),
"userId": user.id,
}
)
await db.disconnect()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -6,6 +6,11 @@ NEXT_PUBLIC_LAUNCHDARKLY_ENABLED=false
NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID=
NEXT_PUBLIC_APP_ENV=dev
## Locale settings
NEXT_PUBLIC_DEFAULT_LOCALE=en
NEXT_PUBLIC_LOCALES=en,es
## Supabase credentials
NEXT_PUBLIC_SUPABASE_URL=http://localhost:8000

View File

@@ -45,3 +45,6 @@ node_modules/
*storybook.log
storybook-static
*.ignore.*
*.ign.*
.cursorrules

View File

@@ -3,12 +3,15 @@ import type { StorybookConfig } from "@storybook/nextjs";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-a11y",
"@storybook/addon-onboarding",
"@storybook/addon-links",
"@storybook/addon-essentials",
"@chromatic-com/storybook",
"@storybook/addon-interactions",
],
features: {
experimentalRSC: true,
},
framework: {
name: "@storybook/nextjs",
options: {},

View File

@@ -1,8 +1,15 @@
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,
@@ -10,6 +17,7 @@ const preview: Preview = {
},
},
},
loaders: [mswLoader],
};
export default preview;

View File

@@ -0,0 +1,22 @@
import type { TestRunnerConfig } from "@storybook/test-runner";
import { injectAxe, checkA11y } from "axe-playwright";
/*
* See https://storybook.js.org/docs/writing-tests/test-runner#test-hook-api
* to learn more about the test-runner hooks API.
*/
const config: TestRunnerConfig = {
async preVisit(page) {
await injectAxe(page);
},
async postVisit(page) {
await checkA11y(page, "#storybook-root", {
detailedReport: true,
detailedReportOptions: {
html: true,
},
});
},
};
export default config;

View File

@@ -7,18 +7,21 @@ RUN --mount=type=cache,target=/usr/local/share/.cache yarn install --frozen-lock
# Dev stage
FROM base AS dev
ENV NODE_ENV=development
ENV HOSTNAME=0.0.0.0
COPY autogpt_platform/frontend/ .
EXPOSE 3000
CMD ["yarn", "run", "dev"]
CMD ["yarn", "run", "dev", "--hostname", "0.0.0.0"]
# Build stage for prod
FROM base AS build
COPY autogpt_platform/frontend/ .
ENV SKIP_STORYBOOK_TESTS=true
RUN yarn build
# Prod stage - based on NextJS reference Dockerfile https://github.com/vercel/next.js/blob/64271354533ed16da51be5dce85f0dbd15f17517/examples/with-docker/Dockerfile
FROM node:21-alpine AS prod
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs

View File

@@ -3,16 +3,16 @@ import { withSentryConfig } from "@sentry/nextjs";
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ["images.unsplash.com"],
},
async redirects() {
return [
{
source: "/monitor", // FIXME: Remove after 2024-09-01
destination: "/",
permanent: false,
},
];
domains: [
"images.unsplash.com",
"ddz4ak4pa3d19.cloudfront.net",
"upload.wikimedia.org",
"storage.googleapis.com",
"picsum.photos", // for placeholder images
"dummyimage.com", // for placeholder images
"placekitten.com", // for placeholder images
],
},
output: "standalone",
// TODO: Re-enable TypeScript checks once current issues are resolved
@@ -46,7 +46,7 @@ export default withSentryConfig(nextConfig, {
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
tunnelRoute: "/monitoring",
tunnelRoute: "/store",
// Hides source maps from generated client bundles
hideSourceMaps: true,

View File

@@ -4,9 +4,9 @@
"private": true,
"scripts": {
"dev": "next dev",
"dev:nosentry": "export NODE_ENV=development && export DISABLE_SENTRY=true && next dev",
"dev:test": "export NODE_ENV=test && next dev",
"build": "next build",
"dev:nosentry": "NODE_ENV=development && DISABLE_SENTRY=true && next dev",
"dev:test": "NODE_ENV=test && next dev",
"build": "SKIP_STORYBOOK_TESTS=true next build",
"start": "next start",
"lint": "next lint && prettier --check .",
"format": "prettier --write .",
@@ -50,13 +50,17 @@
"@tanstack/react-table": "^8.20.5",
"@xyflow/react": "^12.3.6",
"ajv": "^8.17.1",
"boring-avatars": "^1.11.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "1.0.4",
"cookie": "1.0.2",
"date-fns": "^4.1.0",
"dotenv": "^16.4.7",
"elliptic": "6.6.1",
"elliptic": "6.6.0",
"embla-carousel-react": "^8.3.0",
"framer-motion": "^11.11.9",
"geist": "^1.3.1",
"launchdarkly-react-client-sdk": "^3.6.0",
"lucide-react": "^0.468.0",
"moment": "^2.30.1",
@@ -78,24 +82,30 @@
},
"devDependencies": {
"@chromatic-com/storybook": "^3.2.2",
"@playwright/test": "^1.49.0",
"@storybook/addon-essentials": "^8.4.5",
"@storybook/addon-interactions": "^8.4.5",
"@storybook/addon-links": "^8.4.5",
"@storybook/addon-onboarding": "^8.4.5",
"@storybook/blocks": "^8.4.5",
"@storybook/nextjs": "^8.4.5",
"@playwright/test": "^1.48.2",
"@storybook/addon-a11y": "^8.3.5",
"@storybook/addon-essentials": "^8.4.2",
"@storybook/addon-interactions": "^8.4.2",
"@storybook/addon-links": "^8.4.2",
"@storybook/addon-onboarding": "^8.4.2",
"@storybook/blocks": "^8.4.2",
"@storybook/nextjs": "^8.4.2",
"@storybook/react": "^8.3.5",
"@storybook/test": "^8.3.5",
"@storybook/test-runner": "^0.19.1",
"@types/node": "^22.9.3",
"@types/negotiator": "^0.6.3",
"@types/node": "^22.9.0",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-modal": "^3.16.3",
"concurrently": "^9.1.0",
"axe-playwright": "^2.0.3",
"chromatic": "^11.12.5",
"concurrently": "^9.0.1",
"eslint": "^8",
"eslint-config-next": "15.0.3",
"eslint-plugin-storybook": "^0.11.1",
"eslint-plugin-storybook": "^0.11.0",
"msw": "^2.5.2",
"msw-storybook-addon": "^2.0.3",
"postcss": "^8",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.9",
@@ -103,5 +113,10 @@
"tailwindcss": "^3.4.15",
"typescript": "^5"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"msw": {
"workerDirectory": [
"public"
]
}
}

View File

@@ -30,6 +30,7 @@ export default defineConfig({
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
screenshot: 'only-on-failure',
bypassCSP: true,
},
@@ -50,17 +51,17 @@ export default defineConfig({
use: { ...devices["Desktop Safari"] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
// /* Test against mobile viewports. */
// // {
// // name: 'Mobile Chrome',
// // use: { ...devices['Pixel 5'] },
// // },
// // {
// // name: 'Mobile Safari',
// // use: { ...devices['iPhone 12'] },
// // },
/* Test against branded browsers. */
// /* Test against branded browsers. */
{
name: "Microsoft Edge",
use: { ...devices["Desktop Edge"], channel: "msedge" },

View File

@@ -0,0 +1,307 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker.
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/
const PACKAGE_VERSION = '2.6.8'
const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
self.addEventListener('install', function () {
self.skipWaiting()
})
self.addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim())
})
self.addEventListener('message', async function (event) {
const clientId = event.source.id
if (!clientId || !self.clients) {
return
}
const client = await self.clients.get(clientId)
if (!client) {
return
}
const allClients = await self.clients.matchAll({
type: 'window',
})
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: {
packageVersion: PACKAGE_VERSION,
checksum: INTEGRITY_CHECKSUM,
},
})
break
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId)
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: {
client: {
id: client.id,
frameType: client.frameType,
},
},
})
break
}
case 'MOCK_DEACTIVATE': {
activeClientIds.delete(clientId)
break
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId)
const remainingClients = allClients.filter((client) => {
return client.id !== clientId
})
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister()
}
break
}
}
})
self.addEventListener('fetch', function (event) {
const { request } = event
// Bypass navigation requests.
if (request.mode === 'navigate') {
return
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
return
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been deleted (still remains active until the next reload).
if (activeClientIds.size === 0) {
return
}
// Generate unique request ID.
const requestId = crypto.randomUUID()
event.respondWith(handleRequest(event, requestId))
})
async function handleRequest(event, requestId) {
const client = await resolveMainClient(event)
const response = await getResponse(event, client, requestId)
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
;(async function () {
const responseClone = response.clone()
sendToClient(
client,
{
type: 'RESPONSE',
payload: {
requestId,
isMockedResponse: IS_MOCKED_RESPONSE in response,
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
body: responseClone.body,
headers: Object.fromEntries(responseClone.headers.entries()),
},
},
[responseClone.body],
)
})()
}
return response
}
// Resolve the main client for the given event.
// Client that issues a request doesn't necessarily equal the client
// that registered the worker. It's with the latter the worker should
// communicate with during the response resolving phase.
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId)
if (activeClientIds.has(event.clientId)) {
return client
}
if (client?.frameType === 'top-level') {
return client
}
const allClients = await self.clients.matchAll({
type: 'window',
})
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible'
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id)
})
}
async function getResponse(event, client, requestId) {
const { request } = event
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const requestClone = request.clone()
function passthrough() {
// Cast the request headers to a new Headers instance
// so the headers can be manipulated with.
const headers = new Headers(requestClone.headers)
// Remove the "accept" header value that marked this request as passthrough.
// This prevents request alteration and also keeps it compliant with the
// user-defined CORS policies.
const acceptHeader = headers.get('accept')
if (acceptHeader) {
const values = acceptHeader.split(',').map((value) => value.trim())
const filteredValues = values.filter(
(value) => value !== 'msw/passthrough',
)
if (filteredValues.length > 0) {
headers.set('accept', filteredValues.join(', '))
} else {
headers.delete('accept')
}
}
return fetch(requestClone, { headers })
}
// Bypass mocking when the client is not active.
if (!client) {
return passthrough()
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return passthrough()
}
// Notify the client that a request has been intercepted.
const requestBuffer = await request.arrayBuffer()
const clientMessage = await sendToClient(
client,
{
type: 'REQUEST',
payload: {
id: requestId,
url: request.url,
mode: request.mode,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: requestBuffer,
keepalive: request.keepalive,
},
},
[requestBuffer],
)
switch (clientMessage.type) {
case 'MOCK_RESPONSE': {
return respondWithMock(clientMessage.data)
}
case 'PASSTHROUGH': {
return passthrough()
}
}
return passthrough()
}
function sendToClient(client, message, transferrables = []) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error)
}
resolve(event.data)
}
client.postMessage(
message,
[channel.port2].concat(transferrables.filter(Boolean)),
)
})
}
async function respondWithMock(response) {
// Setting response status code to 0 is a no-op.
// However, when responding with a "Response.error()", the produced Response
// instance will have status code set to 0. Since it's not possible to create
// a Response instance with status code 0, handle that use-case separately.
if (response.status === 0) {
return Response.error()
}
const mockedResponse = new Response(response.body, response)
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
value: true,
enumerable: true,
})
return mockedResponse
}

View File

@@ -9,8 +9,7 @@ export default function Home() {
return (
<FlowEditor
className="flow-container"
flowID={query.get("flowID") ?? query.get("templateID") ?? undefined}
template={!!query.get("templateID")}
flowID={query.get("flowID") ?? undefined}
/>
);
}

View File

@@ -0,0 +1,22 @@
{
"auth": {
"signIn": "Sign In",
"email": "Email",
"password": "Password",
"submit": "Submit",
"error": "Invalid login credentials"
},
"dashboard": {
"welcome": "Welcome to your dashboard",
"stats": "Your Stats",
"recentActivity": "Recent Activity"
},
"admin": {
"title": "Admin Dashboard",
"users": "Users Management",
"settings": "System Settings"
},
"home": {
"welcome": "Welcome to the Home Page"
}
}

View File

@@ -0,0 +1,22 @@
{
"auth": {
"signIn": "Iniciar Sesión",
"email": "Correo electrónico",
"password": "Contraseña",
"submit": "Enviar",
"error": "Credenciales inválidas"
},
"dashboard": {
"welcome": "Bienvenido a tu panel",
"stats": "Tus Estadísticas",
"recentActivity": "Actividad Reciente"
},
"admin": {
"title": "Panel de Administración",
"users": "Gestión de Usuarios",
"settings": "Configuración del Sistema"
},
"home": {
"welcome": "Bienvenido a la Página de Inicio"
}
}

View File

@@ -2,6 +2,45 @@
@tailwind components;
@tailwind utilities;
@layer base {
.font-neue {
font-family: "PP Neue Montreal TT", sans-serif;
}
}
@layer utilities {
.w-110 {
width: 27.5rem;
}
.h-7\.5 {
height: 1.1875rem;
}
.h-18 {
height: 4.5rem;
}
.h-238 {
height: 14.875rem;
}
.top-158 {
top: 9.875rem;
}
.top-254 {
top: 15.875rem;
}
.top-284 {
top: 17.75rem;
}
.top-360 {
top: 22.5rem;
}
.left-297 {
left: 18.5625rem;
}
.left-34 {
left: 2.125rem;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;

View File

@@ -0,0 +1,3 @@
export default function HealthPage() {
return <div>Yay im healthy</div>;
}

View File

@@ -2,13 +2,15 @@ import React from "react";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { Providers } from "@/app/providers";
import { NavBar } from "@/components/NavBar";
import { cn } from "@/lib/utils";
import { Navbar } from "@/components/agptui/Navbar";
import "./globals.css";
import TallyPopupSimple from "@/components/TallyPopup";
import { GoogleAnalytics } from "@next/third-parties/google";
import { Toaster } from "@/components/ui/toaster";
import { IconType } from "@/components/ui/icons";
import { createServerClient } from "@/lib/supabase/server";
const inter = Inter({ subsets: ["latin"] });
@@ -17,23 +19,88 @@ export const metadata: Metadata = {
description: "Your one stop shop to creating AI Agents",
};
export default function RootLayout({
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const supabase = createServerClient();
const {
data: { user },
} = await supabase.auth.getUser();
return (
<html lang="en">
<body className={cn("antialiased transition-colors", inter.className)}>
<Providers
initialUser={user}
attribute="class"
defaultTheme="light"
// Feel free to remove this line if you want to use the system theme by default
// enableSystem
disableTransitionOnChange
>
<div className="flex min-h-screen flex-col">
<NavBar />
<div className="flex min-h-screen flex-col items-center justify-center">
<Navbar
user={user}
isLoggedIn={!!user}
links={[
{
name: "Agent Store",
href: "/store",
},
{
name: "Library",
href: "/monitoring",
},
{
name: "Build",
href: "/build",
},
]}
menuItemGroups={[
{
items: [
{
icon: IconType.Edit,
text: "Edit profile",
href: "/store/profile",
},
],
},
{
items: [
{
icon: IconType.LayoutDashboard,
text: "Creator Dashboard",
href: "/store/dashboard",
},
{
icon: IconType.UploadCloud,
text: "Publish an agent",
},
],
},
{
items: [
{
icon: IconType.Settings,
text: "Settings",
href: "/store/settings",
},
],
},
{
items: [
{
icon: IconType.LogOut,
text: "Log out",
},
],
},
]}
/>
<main className="flex-1 p-4">{children}</main>
<TallyPopupSimple />
</div>

View File

@@ -10,6 +10,30 @@ const loginFormSchema = z.object({
password: z.string().min(6).max(64),
});
export async function logout() {
return await Sentry.withServerActionInstrumentation(
"logout",
{},
async () => {
const supabase = createServerClient();
if (!supabase) {
redirect("/error");
}
const { error } = await supabase.auth.signOut();
if (error) {
console.log("Error logging out", error);
return error.message;
}
revalidatePath("/", "layout");
redirect("/login");
},
);
}
export async function login(values: z.infer<typeof loginFormSchema>) {
return await Sentry.withServerActionInstrumentation("login", {}, async () => {
const supabase = createServerClient();
@@ -22,9 +46,10 @@ export async function login(values: z.infer<typeof loginFormSchema>) {
const { data, error } = await supabase.auth.signInWithPassword(values);
if (error) {
console.log("Error logging in", error);
if (error.status == 400) {
// Hence User is not present
redirect("/signup");
redirect("/login");
}
return error.message;
@@ -33,8 +58,44 @@ export async function login(values: z.infer<typeof loginFormSchema>) {
if (data.session) {
await supabase.auth.setSession(data.session);
}
console.log("Logged in");
revalidatePath("/", "layout");
redirect("/");
});
}
export async function signup(values: z.infer<typeof loginFormSchema>) {
"use server";
return await Sentry.withServerActionInstrumentation(
"signup",
{},
async () => {
const supabase = createServerClient();
if (!supabase) {
redirect("/error");
}
// We are sure that the values are of the correct type because zod validates the form
const { data, error } = await supabase.auth.signUp(values);
if (error) {
console.log("Error signing up", error);
if (error.message.includes("P0001")) {
return "Please join our waitlist for your turn: https://agpt.co/waitlist";
}
if (error.code?.includes("user_already_exists")) {
redirect("/login");
}
return error.message;
}
if (data.session) {
await supabase.auth.setSession(data.session);
}
console.log("Signed up");
revalidatePath("/", "layout");
redirect("/store/profile");
},
);
}

View File

@@ -18,7 +18,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { PasswordInput } from "@/components/PasswordInput";
import { FaGoogle, FaGithub, FaDiscord, FaSpinner } from "react-icons/fa";
import { useState } from "react";
import { useSupabase } from "@/components/SupabaseProvider";
import { useSupabase } from "@/components/providers/SupabaseProvider";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { Checkbox } from "@/components/ui/checkbox";
@@ -203,14 +203,34 @@ export default function LoginPage() {
className="flex w-full justify-center"
type="submit"
disabled={isLoading}
onClick={async () => {
setIsLoading(true);
const values = form.getValues();
const result = await login(values);
if (result) {
setFeedback(result);
}
setIsLoading(false);
}}
>
Log in
{isLoading ? <FaSpinner className="animate-spin" /> : "Log in"}
</Button>
<Button
className="flex w-full justify-center"
type="button"
disabled={isLoading}
onClick={async () => {
setIsLoading(true);
const values = form.getValues();
const result = await signup(values);
if (result) {
setFeedback(result);
}
setIsLoading(false);
}}
>
{isLoading ? <FaSpinner className="animate-spin" /> : "Sign up"}
</Button>
</div>
<div className="w-full text-center">
<Link href={"/signup"} className="w-fit text-xs hover:underline">
Create a new Account
</Link>
</div>
</form>
<p className="text-sm text-red-500">{feedback}</p>

View File

@@ -1,41 +0,0 @@
import { Suspense } from "react";
import { notFound } from "next/navigation";
import MarketplaceAPI from "@/lib/marketplace-api";
import { AgentDetailResponse } from "@/lib/marketplace-api";
import AgentDetailContent from "@/components/marketplace/AgentDetailContent";
async function getAgentDetails(id: string): Promise<AgentDetailResponse> {
const apiUrl =
process.env.NEXT_PUBLIC_AGPT_MARKETPLACE_URL ||
"http://localhost:8015/api/v1/market";
const api = new MarketplaceAPI(apiUrl);
try {
console.log(`Fetching agent details for id: ${id}`);
const agent = await api.getAgentDetails(id);
console.log(`Agent details fetched:`, agent);
return agent;
} catch (error) {
console.error(`Error fetching agent details:`, error);
throw error;
}
}
export default async function AgentDetailPage({
params,
}: {
params: { id: string };
}) {
let agent: AgentDetailResponse;
try {
agent = await getAgentDetails(params.id);
} catch (error) {
return notFound();
}
return (
<Suspense fallback={<div>Loading...</div>}>
<AgentDetailContent agent={agent} />
</Suspense>
);
}

View File

@@ -1,352 +0,0 @@
"use client";
import React, { useEffect, useMemo, useState, useCallback } from "react";
import { useRouter } from "next/navigation";
import Image from "next/image";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import MarketplaceAPI, {
AgentResponse,
AgentWithRank,
} from "@/lib/marketplace-api";
import {
ChevronLeft,
ChevronRight,
PlusCircle,
Search,
Star,
} from "lucide-react";
// Utility Functions
function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number,
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout | null = null;
return (...args: Parameters<T>) => {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
// Types
type Agent = AgentResponse | AgentWithRank;
// Components
const HeroSection: React.FC = () => {
const router = useRouter();
return (
<div className="relative bg-indigo-600 py-6">
<div className="absolute inset-0 z-0">
<Image
src="https://images.unsplash.com/photo-1562408590-e32931084e23?auto=format&fit=crop&w=2070&q=80"
alt="Marketplace background"
layout="fill"
objectFit="cover"
quality={75}
priority
className="opacity-20"
/>
<div
className="absolute inset-0 bg-indigo-600 mix-blend-multiply"
aria-hidden="true"
></div>
</div>
<div className="relative mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6 lg:px-8">
<div>
<h1 className="text-2xl font-extrabold tracking-tight text-white sm:text-3xl lg:text-4xl">
AutoGPT Marketplace
</h1>
<p className="mt-2 max-w-3xl text-sm text-indigo-100 sm:text-base">
Discover and share proven AI Agents to supercharge your business.
</p>
</div>
<Button
onClick={() => router.push("/marketplace/submit")}
className="flex items-center bg-white text-indigo-600 hover:bg-indigo-50"
>
<PlusCircle className="mr-2 h-4 w-4" />
Submit Agent
</Button>
</div>
</div>
);
};
const SearchInput: React.FC<{
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}> = ({ value, onChange }) => (
<div className="relative mb-8">
<Input
placeholder="Search agents..."
type="text"
className="w-full rounded-full border-gray-300 py-2 pl-10 pr-4 focus:border-indigo-500 focus:ring-indigo-500"
value={value}
onChange={onChange}
/>
<Search
className="absolute left-3 top-1/2 -translate-y-1/2 transform text-gray-400"
size={20}
/>
</div>
);
const AgentCard: React.FC<{ agent: Agent; featured?: boolean }> = ({
agent,
featured = false,
}) => {
const router = useRouter();
const handleClick = () => {
router.push(`/marketplace/${agent.id}`);
};
return (
<div
className={`flex cursor-pointer flex-col justify-between rounded-lg border p-6 transition-colors duration-200 hover:bg-gray-50 ${featured ? "border-indigo-500 shadow-md" : "border-gray-300"}`}
onClick={handleClick}
>
<div>
<div className="mb-2 flex items-center justify-between">
<h3 className="truncate text-lg font-semibold text-gray-900">
{agent.name}
</h3>
{featured && <Star className="text-indigo-500" size={20} />}
</div>
<p className="mb-4 line-clamp-2 text-sm text-gray-500">
{agent.description}
</p>
<div className="mb-2 text-xs text-gray-400">
Categories: {agent.categories?.join(", ")}
</div>
</div>
<div className="flex items-end justify-between">
<div className="text-xs text-gray-400">
Updated {new Date(agent.updatedAt).toLocaleDateString()}
</div>
<div className="text-xs text-gray-400">Downloads {agent.downloads}</div>
{"rank" in agent && (
<div className="text-xs text-indigo-600">
Rank: {agent.rank.toFixed(2)}
</div>
)}
</div>
</div>
);
};
const AgentGrid: React.FC<{
agents: Agent[];
title: string;
featured?: boolean;
}> = ({ agents, title, featured = false }) => (
<div className="mb-12">
<h2 className="mb-4 text-2xl font-bold text-gray-900">{title}</h2>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{agents.map((agent) => (
<AgentCard agent={agent} key={agent.id} featured={featured} />
))}
</div>
</div>
);
const Pagination: React.FC<{
page: number;
totalPages: number;
onPrevPage: () => void;
onNextPage: () => void;
}> = ({ page, totalPages, onPrevPage, onNextPage }) => (
<div className="mt-8 flex items-center justify-between">
<Button
onClick={onPrevPage}
disabled={page === 1}
className="flex items-center space-x-2 rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50"
>
<ChevronLeft size={16} />
<span>Previous</span>
</Button>
<span className="text-sm text-gray-700">
Page {page} of {totalPages}
</span>
<Button
onClick={onNextPage}
disabled={page === totalPages}
className="flex items-center space-x-2 rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50"
>
<span>Next</span>
<ChevronRight size={16} />
</Button>
</div>
);
// Main Component
const Marketplace: React.FC = () => {
const apiUrl =
process.env.NEXT_PUBLIC_AGPT_MARKETPLACE_URL ||
"http://localhost:8015/api/v1/market";
const api = useMemo(() => new MarketplaceAPI(apiUrl), [apiUrl]);
const [searchValue, setSearchValue] = useState("");
const [searchResults, setSearchResults] = useState<Agent[]>([]);
const [featuredAgents, setFeaturedAgents] = useState<Agent[]>([]);
const [topAgents, setTopAgents] = useState<Agent[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [topAgentsPage, setTopAgentsPage] = useState(1);
const [searchPage, setSearchPage] = useState(1);
const [topAgentsTotalPages, setTopAgentsTotalPages] = useState(1);
const [searchTotalPages, setSearchTotalPages] = useState(1);
const fetchTopAgents = useCallback(
async (currentPage: number) => {
setIsLoading(true);
try {
const response = await api.getTopDownloadedAgents(currentPage, 9);
setTopAgents(response.items);
setTopAgentsTotalPages(response.total_pages);
} catch (error) {
console.error("Error fetching top agents:", error);
} finally {
setIsLoading(false);
}
},
[api],
);
const fetchFeaturedAgents = useCallback(async () => {
try {
const featured = await api.getFeaturedAgents();
setFeaturedAgents(featured.items);
} catch (error) {
console.error("Error fetching featured agents:", error);
}
}, [api]);
const searchAgents = useCallback(
async (searchTerm: string, currentPage: number) => {
setIsLoading(true);
try {
const response = await api.searchAgents(searchTerm, currentPage, 9);
const filteredAgents = response.items.filter((agent) => agent.rank > 0);
setSearchResults(filteredAgents);
setSearchTotalPages(response.total_pages);
} catch (error) {
console.error("Error searching agents:", error);
} finally {
setIsLoading(false);
}
},
[api],
);
const debouncedSearch = useMemo(
() => debounce(searchAgents, 300),
[searchAgents],
);
useEffect(() => {
if (searchValue) {
searchAgents(searchValue, searchPage);
} else {
fetchTopAgents(topAgentsPage);
}
}, [searchValue, searchPage, topAgentsPage, searchAgents, fetchTopAgents]);
useEffect(() => {
fetchFeaturedAgents();
}, [fetchFeaturedAgents]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchValue(e.target.value);
setSearchPage(1);
};
const handleNextPage = () => {
if (searchValue) {
if (searchPage < searchTotalPages) {
setSearchPage(searchPage + 1);
}
} else {
if (topAgentsPage < topAgentsTotalPages) {
setTopAgentsPage(topAgentsPage + 1);
}
}
};
const handlePrevPage = () => {
if (searchValue) {
if (searchPage > 1) {
setSearchPage(searchPage - 1);
}
} else {
if (topAgentsPage > 1) {
setTopAgentsPage(topAgentsPage - 1);
}
}
};
return (
<div className="min-h-screen bg-gray-50">
<HeroSection />
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
<SearchInput value={searchValue} onChange={handleInputChange} />
{isLoading ? (
<div className="py-12 text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-b-2 border-gray-900"></div>
<p className="mt-2 text-gray-600">Loading agents...</p>
</div>
) : searchValue ? (
searchResults.length > 0 ? (
<>
<AgentGrid agents={searchResults} title="Search Results" />
<Pagination
page={searchPage}
totalPages={searchTotalPages}
onPrevPage={handlePrevPage}
onNextPage={handleNextPage}
/>
</>
) : (
<div className="py-12 text-center">
<p className="text-gray-600">
No agents found matching your search criteria.
</p>
</div>
)
) : (
<>
{featuredAgents?.length > 0 ? (
<AgentGrid
agents={featuredAgents}
title="Featured Agents"
featured={true}
/>
) : (
<div className="py-12 text-center">
<p className="text-gray-600">No Featured Agents found</p>
</div>
)}
<hr />
{topAgents?.length > 0 ? (
<AgentGrid agents={topAgents} title="Top Downloaded Agents" />
) : (
<div className="py-12 text-center">
<p className="text-gray-600">No Top Downloaded Agents found</p>
</div>
)}
<Pagination
page={topAgentsPage}
totalPages={topAgentsTotalPages}
onPrevPage={handlePrevPage}
onNextPage={handleNextPage}
/>
</>
)}
</div>
</div>
);
};
export default Marketplace;

View File

@@ -1,453 +0,0 @@
"use client";
import React, { useState, useEffect, useMemo } from "react";
import { useRouter } from "next/navigation";
import { useForm, Controller } from "react-hook-form";
import MarketplaceAPI from "@/lib/marketplace-api";
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
import { Checkbox } from "@/components/ui/checkbox";
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorInput,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from "@/components/ui/multiselect";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
type FormData = {
name: string;
description: string;
author: string;
keywords: string[];
categories: string[];
agreeToTerms: boolean;
selectedAgentId: string;
};
const keywords = [
"Automation",
"AI Workflows",
"Integration",
"Task Automation",
"Data Processing",
"Workflow Management",
"Real-time Analytics",
"Custom Triggers",
"Event-driven",
"API Integration",
"Data Transformation",
"Multi-step Workflows",
"Collaboration Tools",
"Business Process Automation",
"No-code Solutions",
"AI-Powered",
"Smart Notifications",
"Data Syncing",
"User Engagement",
"Reporting Automation",
"Lead Generation",
"Customer Support Automation",
"E-commerce Automation",
"Social Media Management",
"Email Marketing Automation",
"Document Management",
"Data Enrichment",
"Performance Tracking",
"Predictive Analytics",
"Resource Allocation",
"Chatbot",
"Virtual Assistant",
"Workflow Automation",
"Social Media Manager",
"Email Optimizer",
"Content Generator",
"Data Analyzer",
"Task Scheduler",
"Customer Service Bot",
"Personalization Engine",
];
const SubmitPage: React.FC = () => {
const router = useRouter();
const {
control,
handleSubmit,
watch,
setValue,
formState: { errors },
} = useForm<FormData>({
defaultValues: {
selectedAgentId: "", // Initialize with an empty string
name: "",
description: "",
author: "",
keywords: [],
categories: [],
agreeToTerms: false,
},
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const [userAgents, setUserAgents] = useState<
Array<{ id: string; name: string; version: number }>
>([]);
const [selectedAgentGraph, setSelectedAgentGraph] = useState<any>(null);
const selectedAgentId = watch("selectedAgentId");
useEffect(() => {
const fetchUserAgents = async () => {
const api = new AutoGPTServerAPI();
const agents = await api.listGraphs();
console.log(agents);
setUserAgents(
agents.map((agent) => ({
id: agent.id,
name: agent.name || `Agent (${agent.id})`,
version: agent.version,
})),
);
};
fetchUserAgents();
}, []);
useEffect(() => {
const fetchAgentGraph = async () => {
if (selectedAgentId) {
const api = new AutoGPTServerAPI();
const graph = await api.getGraph(selectedAgentId, undefined, true);
setSelectedAgentGraph(graph);
setValue("name", graph.name);
setValue("description", graph.description);
}
};
fetchAgentGraph();
}, [selectedAgentId, setValue]);
const onSubmit = async (data: FormData) => {
setIsSubmitting(true);
setSubmitError(null);
if (!data.agreeToTerms) {
throw new Error("You must agree to the terms of use");
}
try {
if (!selectedAgentGraph) {
throw new Error("Please select an agent");
}
const api = new MarketplaceAPI();
await api.submitAgent(
{
...selectedAgentGraph,
name: data.name,
description: data.description,
},
data.author,
data.keywords,
data.categories,
);
router.push("/marketplace?submission=success");
} catch (error) {
console.error("Submission error:", error);
setSubmitError(
error instanceof Error ? error.message : "An unknown error occurred",
);
} finally {
setIsSubmitting(false);
}
};
return (
<div className="container mx-auto px-4 py-8">
<h1 className="mb-6 text-3xl font-bold">Submit Your Agent</h1>
<Card className="p-6">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-4">
<Controller
name="selectedAgentId"
control={control}
rules={{ required: "Please select an agent" }}
render={({ field }) => (
<div>
<label
htmlFor={field.name}
className="block text-sm font-medium text-gray-700"
>
Select Agent
</label>
<Select
onValueChange={field.onChange}
value={field.value || ""}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an agent" />
</SelectTrigger>
<SelectContent>
{userAgents.map((agent) => (
<SelectItem key={agent.id} value={agent.id}>
{agent.name} (v{agent.version})
</SelectItem>
))}
</SelectContent>
</Select>
{errors.selectedAgentId && (
<p className="mt-1 text-sm text-red-600">
{errors.selectedAgentId.message}
</p>
)}
</div>
)}
/>
{/* {selectedAgentGraph && (
<div className="mt-4" style={{ height: "600px" }}>
<ReactFlow
nodes={nodes}
edges={edges}
fitView
attributionPosition="bottom-left"
nodesConnectable={false}
nodesDraggable={false}
zoomOnScroll={false}
panOnScroll={false}
elementsSelectable={false}
>
<Controls showInteractive={false} />
<Background />
</ReactFlow>
</div>
)} */}
<Controller
name="name"
control={control}
rules={{ required: "Name is required" }}
render={({ field }) => (
<div>
<label
htmlFor={field.name}
className="block text-sm font-medium text-gray-700"
>
Agent Name
</label>
<Input
id={field.name}
placeholder="Enter your agent's name"
{...field}
/>
{errors.name && (
<p className="mt-1 text-sm text-red-600">
{errors.name.message}
</p>
)}
</div>
)}
/>
<Controller
name="description"
control={control}
rules={{ required: "Description is required" }}
render={({ field }) => (
<div>
<label
htmlFor={field.name}
className="block text-sm font-medium text-gray-700"
>
Description
</label>
<Textarea
id={field.name}
placeholder="Describe your agent"
{...field}
/>
{errors.description && (
<p className="mt-1 text-sm text-red-600">
{errors.description.message}
</p>
)}
</div>
)}
/>
<Controller
name="author"
control={control}
rules={{ required: "Author is required" }}
render={({ field }) => (
<div>
<label
htmlFor={field.name}
className="block text-sm font-medium text-gray-700"
>
Author
</label>
<Input
id={field.name}
placeholder="Your name or username"
{...field}
/>
{errors.author && (
<p className="mt-1 text-sm text-red-600">
{errors.author.message}
</p>
)}
</div>
)}
/>
<Controller
name="keywords"
control={control}
rules={{ required: "At least one keyword is required" }}
render={({ field }) => (
<div>
<label
htmlFor={field.name}
className="block text-sm font-medium text-gray-700"
>
Keywords
</label>
<MultiSelector
values={field.value || []}
onValuesChange={field.onChange}
>
<MultiSelectorTrigger>
<MultiSelectorInput placeholder="Add keywords" />
</MultiSelectorTrigger>
<MultiSelectorContent>
<MultiSelectorList>
{keywords.map((keyword) => (
<MultiSelectorItem key={keyword} value={keyword}>
{keyword}
</MultiSelectorItem>
))}
{/* Add more predefined keywords as needed */}
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
{errors.keywords && (
<p className="mt-1 text-sm text-red-600">
{errors.keywords.message}
</p>
)}
</div>
)}
/>
<Controller
name="categories"
control={control}
rules={{ required: "At least one category is required" }}
render={({ field }) => (
<div>
<label
htmlFor={field.name}
className="block text-sm font-medium text-gray-700"
>
Categories
</label>
<MultiSelector
values={field.value || []}
onValuesChange={field.onChange}
>
<MultiSelectorTrigger>
<MultiSelectorInput placeholder="Select categories" />
</MultiSelectorTrigger>
<MultiSelectorContent>
<MultiSelectorList>
<MultiSelectorItem value="productivity">
Productivity
</MultiSelectorItem>
<MultiSelectorItem value="entertainment">
Entertainment
</MultiSelectorItem>
<MultiSelectorItem value="education">
Education
</MultiSelectorItem>
<MultiSelectorItem value="business">
Business
</MultiSelectorItem>
<MultiSelectorItem value="other">
Other
</MultiSelectorItem>
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
{errors.categories && (
<p className="mt-1 text-sm text-red-600">
{errors.categories.message}
</p>
)}
</div>
)}
/>
<Controller
name="agreeToTerms"
control={control}
rules={{ required: "You must agree to the terms of use" }}
render={({ field }) => (
<div className="flex items-center space-x-2">
<Checkbox
id="agreeToTerms"
checked={field.value}
onCheckedChange={field.onChange}
/>
<label
htmlFor="agreeToTerms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
I agree to the{" "}
<a
href="https://auto-gpt.notion.site/Terms-of-Use-11400ef5bece80d0b087d7831c5fd6bf"
className="text-blue-500 hover:underline"
>
terms of use
</a>
</label>
</div>
)}
/>
{errors.agreeToTerms && (
<p className="mt-1 text-sm text-red-600">
{errors.agreeToTerms.message}
</p>
)}
{submitError && (
<Alert variant="destructive">
<AlertTitle>Submission Failed</AlertTitle>
<AlertDescription>{submitError}</AlertDescription>
</Alert>
)}
<Button type="submit" className="w-full" disabled={isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit Agent"}
</Button>
</div>
</form>
</Card>
</div>
);
};
export default SubmitPage;

View File

@@ -0,0 +1,168 @@
"use client";
import { useEffect, useState, useMemo, useCallback } from "react";
import AutoGPTServerAPI, {
GraphMetaWithRuns,
ExecutionMeta,
Schedule,
} from "@/lib/autogpt-server-api";
import { Card } from "@/components/ui/card";
import { FlowRun } from "@/lib/types";
import {
AgentFlowList,
FlowInfo,
FlowRunInfo,
FlowRunsList,
FlowRunsStats,
} from "@/components/monitor";
import { SchedulesTable } from "@/components/monitor/scheduleTable";
const Monitor = () => {
const [flows, setFlows] = useState<GraphMetaWithRuns[]>([]);
const [flowRuns, setFlowRuns] = useState<FlowRun[]>([]);
const [schedules, setSchedules] = useState<Schedule[]>([]);
const [selectedFlow, setSelectedFlow] = useState<GraphMetaWithRuns | null>(
null,
);
const [selectedRun, setSelectedRun] = useState<FlowRun | null>(null);
const [sortColumn, setSortColumn] = useState<keyof Schedule>("id");
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
const api = useMemo(() => new AutoGPTServerAPI(), []);
const fetchSchedules = useCallback(async () => {
setSchedules(await api.listSchedules());
}, [api]);
const removeSchedule = useCallback(
async (scheduleId: string) => {
const removedSchedule = await api.deleteSchedule(scheduleId);
setSchedules(schedules.filter((s) => s.id !== removedSchedule.id));
},
[schedules, api],
);
const fetchAgents = useCallback(() => {
api.listGraphsWithRuns().then((agent) => {
setFlows(agent);
const flowRuns = agent.flatMap((graph) =>
graph.executions != null
? graph.executions.map((execution) =>
flowRunFromExecutionMeta(graph, execution),
)
: [],
);
setFlowRuns(flowRuns);
});
}, [api]);
useEffect(() => {
fetchAgents();
}, [fetchAgents]);
useEffect(() => {
fetchSchedules();
}, [fetchSchedules]);
useEffect(() => {
const intervalId = setInterval(() => fetchAgents(), 5000);
return () => clearInterval(intervalId);
}, [fetchAgents, flows]);
const column1 = "md:col-span-2 xl:col-span-3 xxl:col-span-2";
const column2 = "md:col-span-3 lg:col-span-2 xl:col-span-3";
const column3 = "col-span-full xl:col-span-4 xxl:col-span-5";
const handleSort = (column: keyof Schedule) => {
if (sortColumn === column) {
setSortDirection(sortDirection === "asc" ? "desc" : "asc");
} else {
setSortColumn(column);
setSortDirection("asc");
}
};
return (
<div
className="grid h-full w-screen grid-cols-1 gap-4 px-8 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10"
data-testid="monitor-page"
>
<AgentFlowList
className={column1}
flows={flows}
flowRuns={flowRuns}
selectedFlow={selectedFlow}
onSelectFlow={(f) => {
setSelectedRun(null);
setSelectedFlow(
f.id == selectedFlow?.id ? null : (f as GraphMetaWithRuns),
);
}}
/>
<FlowRunsList
className={column2}
flows={flows}
runs={[
...(selectedFlow
? flowRuns.filter((v) => v.graphID == selectedFlow.id)
: flowRuns),
].sort((a, b) => Number(a.startTime) - Number(b.startTime))}
selectedRun={selectedRun}
onSelectRun={(r) => setSelectedRun(r.id == selectedRun?.id ? null : r)}
/>
{(selectedRun && (
<FlowRunInfo
flow={selectedFlow || flows.find((f) => f.id == selectedRun.graphID)!}
flowRun={selectedRun}
className={column3}
/>
)) ||
(selectedFlow && (
<FlowInfo
flow={selectedFlow}
flowRuns={flowRuns.filter((r) => r.graphID == selectedFlow.id)}
className={column3}
refresh={() => {
fetchAgents();
setSelectedFlow(null);
setSelectedRun(null);
}}
/>
)) || (
<Card className={`p-6 ${column3}`}>
<FlowRunsStats flows={flows} flowRuns={flowRuns} />
</Card>
)}
<div className="col-span-full xl:col-span-6">
<SchedulesTable
schedules={schedules} // all schedules
agents={flows} // for filtering purpose
onRemoveSchedule={removeSchedule}
sortColumn={sortColumn}
sortDirection={sortDirection}
onSort={handleSort}
/>
</div>
</div>
);
};
function flowRunFromExecutionMeta(
graphMeta: GraphMetaWithRuns,
executionMeta: ExecutionMeta,
): FlowRun {
return {
id: executionMeta.execution_id,
graphID: graphMeta.id,
graphVersion: graphMeta.version,
status: executionMeta.status,
startTime: executionMeta.started_at,
endTime: executionMeta.ended_at,
duration: executionMeta.duration,
totalRunTime: executionMeta.total_run_time,
} as FlowRun;
}
export default Monitor;

View File

@@ -1,145 +1,7 @@
"use client";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import AutoGPTServerAPI, {
GraphExecution,
Schedule,
GraphMeta,
} from "@/lib/autogpt-server-api";
import { redirect } from "next/navigation";
import { Card } from "@/components/ui/card";
import {
AgentFlowList,
FlowInfo,
FlowRunInfo,
FlowRunsList,
FlowRunsStats,
} from "@/components/monitor";
import { SchedulesTable } from "@/components/monitor/scheduleTable";
const Monitor = () => {
const [flows, setFlows] = useState<GraphMeta[]>([]);
const [executions, setExecutions] = useState<GraphExecution[]>([]);
const [schedules, setSchedules] = useState<Schedule[]>([]);
const [selectedFlow, setSelectedFlow] = useState<GraphMeta | null>(null);
const [selectedRun, setSelectedRun] = useState<GraphExecution | null>(null);
const [sortColumn, setSortColumn] = useState<keyof Schedule>("id");
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
const api = useMemo(() => new AutoGPTServerAPI(), []);
const fetchSchedules = useCallback(async () => {
setSchedules(await api.listSchedules());
}, [api]);
const removeSchedule = useCallback(
async (scheduleId: string) => {
const removedSchedule = await api.deleteSchedule(scheduleId);
setSchedules(schedules.filter((s) => s.id !== removedSchedule.id));
},
[schedules, api],
);
const fetchAgents = useCallback(() => {
api.listGraphs().then((agent) => {
setFlows(agent);
});
api.getExecutions().then((executions) => {
setExecutions(executions);
});
}, [api]);
useEffect(() => {
fetchAgents();
}, [fetchAgents]);
useEffect(() => {
fetchSchedules();
}, [fetchSchedules]);
useEffect(() => {
const intervalId = setInterval(() => fetchAgents(), 5000);
return () => clearInterval(intervalId);
}, [fetchAgents, flows]);
const column1 = "md:col-span-2 xl:col-span-3 xxl:col-span-2";
const column2 = "md:col-span-3 lg:col-span-2 xl:col-span-3";
const column3 = "col-span-full xl:col-span-4 xxl:col-span-5";
const handleSort = (column: keyof Schedule) => {
if (sortColumn === column) {
setSortDirection(sortDirection === "asc" ? "desc" : "asc");
} else {
setSortColumn(column);
setSortDirection("asc");
}
};
return (
<div
className="grid grid-cols-1 gap-4 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10"
data-testid="monitor-page"
>
<AgentFlowList
className={column1}
flows={flows}
executions={executions}
selectedFlow={selectedFlow}
onSelectFlow={(f) => {
setSelectedRun(null);
setSelectedFlow(f.id == selectedFlow?.id ? null : (f as GraphMeta));
}}
/>
<FlowRunsList
className={column2}
flows={flows}
executions={[
...(selectedFlow
? executions.filter((v) => v.graph_id == selectedFlow.id)
: executions),
].sort((a, b) => Number(b.started_at) - Number(a.started_at))}
selectedRun={selectedRun}
onSelectRun={(r) =>
setSelectedRun(r.execution_id == selectedRun?.execution_id ? null : r)
}
/>
{(selectedRun && (
<FlowRunInfo
flow={
selectedFlow || flows.find((f) => f.id == selectedRun.graph_id)!
}
execution={selectedRun}
className={column3}
/>
)) ||
(selectedFlow && (
<FlowInfo
flow={selectedFlow}
executions={executions.filter((e) => e.graph_id == selectedFlow.id)}
className={column3}
refresh={() => {
fetchAgents();
setSelectedFlow(null);
setSelectedRun(null);
}}
/>
)) || (
<Card className={`p-6 ${column3}`}>
<FlowRunsStats flows={flows} executions={executions} />
</Card>
)}
<div className="col-span-full xl:col-span-6">
<SchedulesTable
schedules={schedules} // all schedules
agents={flows} // for filtering purpose
onRemoveSchedule={removeSchedule}
sortColumn={sortColumn}
sortDirection={sortDirection}
onSort={handleSort}
/>
</div>
</div>
);
};
export default Monitor;
export default function Page() {
redirect("/store");
}

View File

@@ -1,6 +1,6 @@
"use client";
import { useSupabase } from "@/components/SupabaseProvider";
import { useSupabase } from "@/components/providers/SupabaseProvider";
import { Button } from "@/components/ui/button";
import useUser from "@/hooks/useUser";
import { useRouter } from "next/navigation";

View File

@@ -2,17 +2,22 @@
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { ThemeProviderProps } from "next-themes/dist/types";
import { BackendAPIProvider } from "@/lib/autogpt-server-api";
import { ThemeProviderProps } from "next-themes";
import { BackendAPIProvider } from "@/lib/autogpt-server-api/context";
import { TooltipProvider } from "@/components/ui/tooltip";
import SupabaseProvider from "@/components/SupabaseProvider";
import SupabaseProvider from "@/components/providers/SupabaseProvider";
import CredentialsProvider from "@/components/integrations/credentials-provider";
import { User } from "@supabase/supabase-js";
import { LaunchDarklyProvider } from "@/components/feature-flag/feature-flag-provider";
export function Providers({ children, ...props }: ThemeProviderProps) {
export function Providers({
children,
initialUser,
...props
}: ThemeProviderProps & { initialUser: User | null }) {
return (
<NextThemesProvider {...props}>
<SupabaseProvider>
<SupabaseProvider initialUser={initialUser}>
<BackendAPIProvider>
<CredentialsProvider>
<LaunchDarklyProvider>

View File

@@ -1,46 +0,0 @@
"use server";
import { createServerClient } from "@/lib/supabase/server";
import * as Sentry from "@sentry/nextjs";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { z } from "zod";
const SignupFormSchema = z.object({
email: z.string().email().min(2).max(64),
password: z.string().min(6).max(64),
});
export async function signup(values: z.infer<typeof SignupFormSchema>) {
"use server";
return await Sentry.withServerActionInstrumentation(
"signup",
{},
async () => {
const supabase = createServerClient();
if (!supabase) {
redirect("/error");
}
// We are sure that the values are of the correct type because zod validates the form
const { data, error } = await supabase.auth.signUp(values);
if (error) {
if (error.message.includes("P0001")) {
return "Please join our waitlist for your turn: https://agpt.co/waitlist";
}
if (error.code?.includes("user_already_exists")) {
redirect("/login");
}
return error.message;
}
if (data.session) {
await supabase.auth.setSession(data.session);
}
revalidatePath("/", "layout");
redirect("/");
},
);
}

View File

@@ -1,225 +0,0 @@
"use client";
import useUser from "@/hooks/useUser";
import { signup } from "./actions";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { useForm } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { PasswordInput } from "@/components/PasswordInput";
import { FaGoogle, FaGithub, FaDiscord, FaSpinner } from "react-icons/fa";
import { useState } from "react";
import { useSupabase } from "@/components/SupabaseProvider";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { Checkbox } from "@/components/ui/checkbox";
const signupFormSchema = z.object({
email: z.string().email().min(2).max(64),
password: z.string().min(6).max(64),
agreeToTerms: z.boolean().refine((value) => value === true, {
message: "You must agree to the Terms of Use and Privacy Policy",
}),
});
export default function LoginPage() {
const { supabase, isLoading: isSupabaseLoading } = useSupabase();
const { user, isLoading: isUserLoading } = useUser();
const [feedback, setFeedback] = useState<string | null>(null);
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const form = useForm<z.infer<typeof signupFormSchema>>({
resolver: zodResolver(signupFormSchema),
defaultValues: {
email: "",
password: "",
agreeToTerms: false,
},
});
if (user) {
console.log("User exists, redirecting to home");
router.push("/");
}
if (isUserLoading || isSupabaseLoading || user) {
return (
<div className="flex h-[80vh] items-center justify-center">
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
</div>
);
}
if (!supabase) {
return (
<div>
User accounts are disabled because Supabase client is unavailable
</div>
);
}
async function handleSignInWithProvider(
provider: "google" | "github" | "discord",
) {
const { data, error } = await supabase!.auth.signInWithOAuth({
provider: provider,
options: {
redirectTo:
process.env.AUTH_CALLBACK_URL ??
`http://localhost:3000/auth/callback`,
},
});
if (!error) {
setFeedback(null);
return;
}
setFeedback(error.message);
}
const onSignup = async (data: z.infer<typeof signupFormSchema>) => {
if (await form.trigger()) {
setIsLoading(true);
const error = await signup(data);
setIsLoading(false);
if (error) {
setFeedback(error);
return;
}
setFeedback(null);
}
};
return (
<div className="flex h-[80vh] items-center justify-center">
<div className="w-full max-w-md space-y-6 rounded-lg p-8 shadow-md">
<h1 className="text-lg font-medium">Create a New Account</h1>
{/* <div className="mb-6 space-y-2">
<Button
className="w-full"
onClick={() => handleSignInWithProvider("google")}
variant="outline"
type="button"
disabled={isLoading}
>
<FaGoogle className="mr-2 h-4 w-4" />
Sign in with Google
</Button>
<Button
className="w-full"
onClick={() => handleSignInWithProvider("github")}
variant="outline"
type="button"
disabled={isLoading}
>
<FaGithub className="mr-2 h-4 w-4" />
Sign in with GitHub
</Button>
<Button
className="w-full"
onClick={() => handleSignInWithProvider("discord")}
variant="outline"
type="button"
disabled={isLoading}
>
<FaDiscord className="mr-2 h-4 w-4" />
Sign in with Discord
</Button>
</div> */}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSignup)}>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem className="mb-4">
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="user@email.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<PasswordInput placeholder="password" {...field} />
</FormControl>
<FormDescription>
Password needs to be at least 6 characters long
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="agreeToTerms"
render={({ field }) => (
<FormItem className="mt-4 flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
I agree to the{" "}
<Link
href="https://auto-gpt.notion.site/Terms-of-Use-11400ef5bece80d0b087d7831c5fd6bf"
className="underline"
>
Terms of Use
</Link>{" "}
and{" "}
<Link
href="https://www.notion.so/auto-gpt/Privacy-Policy-ab11c9c20dbd4de1a15dcffe84d77984"
className="underline"
>
Privacy Policy
</Link>
</FormLabel>
<FormMessage />
</div>
</FormItem>
)}
/>
<div className="mb-6 mt-8 flex w-full space-x-4">
<Button
className="flex w-full justify-center"
variant="outline"
type="button"
onClick={form.handleSubmit(onSignup)}
disabled={isLoading}
>
Sign up
</Button>
</div>
<div className="w-full text-center">
<Link href={"/login"} className="w-fit text-xs hover:underline">
Already a member? Log In here
</Link>
</div>
</form>
<p className="text-sm text-red-500">{feedback}</p>
</Form>
</div>
</div>
);
}

View File

@@ -0,0 +1,167 @@
"use client";
import * as React from "react";
import { AgentTable } from "@/components/agptui/AgentTable";
import { AgentTableRowProps } from "@/components/agptui/AgentTableRow";
import { Button } from "@/components/agptui/Button";
import { Separator } from "@/components/ui/separator";
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
import { createClient } from "@/lib/supabase/client";
import { StatusType } from "@/components/agptui/Status";
import { PublishAgentPopout } from "@/components/agptui/composite/PublishAgentPopout";
import { useCallback, useEffect, useState } from "react";
import {
StoreSubmissionsResponse,
StoreSubmissionRequest,
} from "@/lib/autogpt-server-api/types";
async function getDashboardData() {
const supabase = createClient();
if (!supabase) {
return { submissions: [] };
}
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
console.warn("--- No session found in profile page");
return { profile: null };
}
const api = new AutoGPTServerAPI(
process.env.NEXT_PUBLIC_AGPT_SERVER_URL,
process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL,
supabase,
);
try {
const submissions = await api.getStoreSubmissions();
return {
submissions,
};
} catch (error) {
console.error("Error fetching profile:", error);
return {
profile: null,
};
}
}
export default function Page({}: {}) {
const [submissions, setSubmissions] = useState<StoreSubmissionsResponse>();
const [openPopout, setOpenPopout] = useState<boolean>(false);
const [submissionData, setSubmissionData] =
useState<StoreSubmissionRequest>();
const [popoutStep, setPopoutStep] = useState<"select" | "info" | "review">(
"info",
);
const fetchData = useCallback(async () => {
const { submissions } = await getDashboardData();
if (submissions) {
setSubmissions(submissions as StoreSubmissionsResponse);
}
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
const onEditSubmission = useCallback((submission: StoreSubmissionRequest) => {
setSubmissionData(submission);
setPopoutStep("review");
setOpenPopout(true);
}, []);
const onDeleteSubmission = useCallback(
(submission_id: string) => {
const supabase = createClient();
if (!supabase) {
return;
}
const api = new AutoGPTServerAPI(
process.env.NEXT_PUBLIC_AGPT_SERVER_URL,
process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL,
supabase,
);
api.deleteStoreSubmission(submission_id);
fetchData();
},
[fetchData],
);
const onOpenPopout = useCallback(() => {
setPopoutStep("select");
setOpenPopout(true);
}, []);
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">
Submit a New Agent
</h2>
<p className="text-sm text-[#707070] dark:text-neutral-400">
Select from the list of agents you currently have, or upload from
your local machine.
</p>
</div>
</div>
<PublishAgentPopout
trigger={
<Button
variant="default"
size="sm"
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>
<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,
})) as AgentTableRowProps[]) || []
}
onEditSubmission={onEditSubmission}
onDeleteSubmission={onDeleteSubmission}
/>
</div>
</main>
);
}

View File

@@ -0,0 +1,23 @@
import * as React from "react";
import { Sidebar } from "@/components/agptui/Sidebar";
export default function Layout({ children }: { children: React.ReactNode }) {
const sidebarLinkGroups = [
{
links: [
{ text: "Creator Dashboard", href: "/store/dashboard" },
{ text: "Agent dashboard", href: "/store/agent-dashboard" },
{ text: "Integrations", href: "/store/integrations" },
{ text: "Profile", href: "/store/profile" },
{ text: "Settings", href: "/store/settings" },
],
},
];
return (
<div className="flex min-h-screen w-screen max-w-[1360px] flex-col lg:flex-row">
<Sidebar linkGroups={sidebarLinkGroups} />
<div className="pl-4">{children}</div>
</div>
);
}

View File

@@ -0,0 +1,55 @@
import * as React from "react";
import { ProfileInfoForm } from "@/components/agptui/ProfileInfoForm";
import AutoGPTServerAPIServerSide from "@/lib/autogpt-server-api";
import { createServerClient } from "@/lib/supabase/server";
import { CreatorDetails } from "@/lib/autogpt-server-api/types";
async function getProfileData() {
// Get the supabase client first
const supabase = createServerClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
console.warn("--- No session found in profile page");
return { profile: null };
}
// Create API client with the same supabase instance
const api = new AutoGPTServerAPIServerSide(
process.env.NEXT_PUBLIC_AGPT_SERVER_URL,
process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL,
supabase, // Pass the supabase client instance
);
try {
const profile = await api.getStoreProfile("profile");
return {
profile,
};
} catch (error) {
console.error("Error fetching profile:", error);
return {
profile: null,
};
}
}
export default async function Page({}: {}) {
const { profile } = await getProfileData();
if (!profile) {
return (
<div className="flex flex-col items-center justify-center p-4">
<p>Please log in to view your profile</p>
</div>
);
}
return (
<div className="flex flex-col items-center justify-center px-4">
<ProfileInfoForm profile={profile as CreatorDetails} />
</div>
);
}

View File

@@ -0,0 +1,6 @@
import * as React from "react";
import { SettingsInputForm } from "@/components/agptui/SettingsInputForm";
export default function Page() {
return <SettingsInputForm />;
}

View File

@@ -0,0 +1,90 @@
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
import { BreadCrumbs } from "@/components/agptui/BreadCrumbs";
import { AgentInfo } from "@/components/agptui/AgentInfo";
import { AgentImages } from "@/components/agptui/AgentImages";
import { AgentsSection } from "@/components/agptui/composite/AgentsSection";
import { BecomeACreator } from "@/components/agptui/BecomeACreator";
import { Separator } from "@/components/ui/separator";
import { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: { creator: string; slug: string };
}): Promise<Metadata> {
const api = new AutoGPTServerAPI();
const agent = await api.getStoreAgent(params.creator, params.slug);
return {
title: `${agent.agent_name} - AutoGPT Store`,
description: agent.description,
};
}
// export async function generateStaticParams() {
// const api = new AutoGPTServerAPI();
// const agents = await api.getStoreAgents({ featured: true });
// return agents.agents.map((agent) => ({
// creator: agent.creator,
// slug: agent.slug,
// }));
// }
export default async function Page({
params,
}: {
params: { creator: string; slug: string };
}) {
const api = new AutoGPTServerAPI();
const agent = await api.getStoreAgent(params.creator, params.slug);
const otherAgents = await api.getStoreAgents({ creator: params.creator });
const similarAgents = await api.getStoreAgents({
search_query: agent.categories[0],
});
const breadcrumbs = [
{ name: "Store", link: "/store" },
{ name: agent.creator, link: `/store/creator/${agent.creator}` },
{ name: agent.agent_name, link: "#" },
];
return (
<div className="mx-auto w-screen max-w-[1360px]">
<main className="px-4 md:mt-4 lg:mt-8">
<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
name={agent.agent_name}
creator={agent.creator}
shortDescription={agent.description}
longDescription={agent.description}
rating={agent.rating}
runs={agent.runs}
categories={agent.categories}
lastUpdated={agent.updated_at}
version={agent.versions[agent.versions.length - 1]}
/>
</div>
<AgentImages images={agent.agent_image} />
</div>
<Separator className="my-6" />
<AgentsSection
agents={otherAgents.agents}
sectionTitle={`Other agents by ${agent.creator}`}
/>
<Separator className="my-6" />
<AgentsSection
agents={similarAgents.agents}
sectionTitle="Similar agents"
/>
<BecomeACreator
title="Become a Creator"
description="Join our ever-growing community of hackers and tinkerers"
buttonText="Become a Creator"
/>
</main>
</div>
);
}

View File

@@ -0,0 +1,93 @@
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
import {
CreatorDetails as Creator,
StoreAgent,
} from "@/lib/autogpt-server-api";
import { AgentsSection } from "@/components/agptui/composite/AgentsSection";
import { BreadCrumbs } from "@/components/agptui/BreadCrumbs";
import { Metadata } from "next";
import { CreatorInfoCard } from "@/components/agptui/CreatorInfoCard";
import { CreatorLinks } from "@/components/agptui/CreatorLinks";
export async function generateMetadata({
params,
}: {
params: { creator: string };
}): Promise<Metadata> {
const api = new AutoGPTServerAPI();
const creator = await api.getStoreCreator(params.creator);
return {
title: `${creator.name} - AutoGPT Store`,
description: creator.description,
};
}
// export async function generateStaticParams() {
// const api = new AutoGPTServerAPI();
// const creators = await api.getStoreCreators({ featured: true });
// return creators.creators.map((creator) => ({
// creator: creator.username,
// }));
// }
export default async function Page({
params,
}: {
params: { creator: string };
}) {
const api = new AutoGPTServerAPI();
try {
const creator = await api.getStoreCreator(params.creator);
const creatorAgents = await api.getStoreAgents({ creator: params.creator });
return (
<div className="mx-auto w-screen max-w-[1360px]">
<main className="px-4 md:mt-4 lg:mt-8">
<BreadCrumbs
items={[
{ name: "Store", link: "/store" },
{ 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">
<div className="font-neue text-2xl font-normal leading-normal text-neutral-900 sm:text-3xl md:text-[35px] md:leading-[45px]">
{creator.description}
</div>
<CreatorLinks links={creator.links} />
</div>
</div>
<div className="mt-8 sm:mt-12 md:mt-16">
<hr className="w-full bg-neutral-700" />
<AgentsSection
agents={creatorAgents.agents}
hideAvatars={true}
sectionTitle={`Agents by ${creator.name}`}
/>
</div>
</main>
</div>
);
} catch (error) {
return (
<div className="flex h-screen w-full items-center justify-center">
<div className="font-neue text-2xl text-neutral-900">
Creator not found
</div>
</div>
);
}
}

View File

@@ -0,0 +1,191 @@
import * as React from "react";
import { HeroSection } from "@/components/agptui/composite/HeroSection";
import {
FeaturedSection,
FeaturedAgent,
} from "@/components/agptui/composite/FeaturedSection";
import {
AgentsSection,
Agent,
} from "@/components/agptui/composite/AgentsSection";
import { BecomeACreator } from "@/components/agptui/BecomeACreator";
import {
FeaturedCreators,
FeaturedCreator,
} from "@/components/agptui/composite/FeaturedCreators";
import { Separator } from "@/components/ui/separator";
import AutoGPTServerAPIServerSide from "@/lib/autogpt-server-api/clientServer";
import { Metadata } from "next";
import { createServerClient } from "@/lib/supabase/server";
import {
StoreAgentsResponse,
CreatorsResponse,
} from "@/lib/autogpt-server-api/types";
export const dynamic = "force-dynamic";
async function getStoreData() {
try {
const supabase = createServerClient();
const {
data: { session },
} = await supabase.auth.getSession();
const api = new AutoGPTServerAPIServerSide(
process.env.NEXT_PUBLIC_AGPT_SERVER_URL,
process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL,
supabase,
);
// Add error handling and default values
let featuredAgents: StoreAgentsResponse = {
agents: [],
pagination: {
total_items: 0,
total_pages: 0,
current_page: 0,
page_size: 0,
},
};
let topAgents: StoreAgentsResponse = {
agents: [],
pagination: {
total_items: 0,
total_pages: 0,
current_page: 0,
page_size: 0,
},
};
let featuredCreators: CreatorsResponse = {
creators: [],
pagination: {
total_items: 0,
total_pages: 0,
current_page: 0,
page_size: 0,
},
};
try {
[featuredAgents, topAgents, featuredCreators] = await Promise.all([
api.getStoreAgents({ featured: true }),
api.getStoreAgents({ sorted_by: "runs" }),
api.getStoreCreators({ featured: true, sorted_by: "num_agents" }),
]);
} catch (error) {
console.error("Error fetching store data:", error);
}
return {
featuredAgents,
topAgents,
featuredCreators,
};
} catch (error) {
console.error("Error in getStoreData:", error);
return {
featuredAgents: {
agents: [],
pagination: {
total_items: 0,
total_pages: 0,
current_page: 0,
page_size: 0,
},
},
topAgents: {
agents: [],
pagination: {
total_items: 0,
total_pages: 0,
current_page: 0,
page_size: 0,
},
},
featuredCreators: {
creators: [],
pagination: {
total_items: 0,
total_pages: 0,
current_page: 0,
page_size: 0,
},
},
};
}
}
// FIX: Correct metadata
export const metadata: Metadata = {
title: "Agent Store - NextGen AutoGPT",
description: "Find and use AI Agents created by our community",
applicationName: "NextGen AutoGPT Store",
authors: [{ name: "AutoGPT Team" }],
keywords: [
"AI agents",
"automation",
"artificial intelligence",
"AutoGPT",
"marketplace",
],
robots: {
index: true,
follow: true,
},
openGraph: {
title: "Agent Store - NextGen AutoGPT",
description: "Find and use AI Agents created by our community",
type: "website",
siteName: "NextGen AutoGPT Store",
images: [
{
url: "/images/store-og.png",
width: 1200,
height: 630,
alt: "NextGen AutoGPT Store",
},
],
},
twitter: {
card: "summary_large_image",
title: "Agent Store - NextGen AutoGPT",
description: "Find and use AI Agents created by our community",
images: ["/images/store-twitter.png"],
},
icons: {
icon: "/favicon.ico",
shortcut: "/favicon-16x16.png",
apple: "/apple-touch-icon.png",
},
};
export default async function Page({}: {}) {
// Get data server-side
const { featuredAgents, topAgents, featuredCreators } = await getStoreData();
return (
<div className="mx-auto w-screen max-w-[1360px]">
<main className="px-4">
<HeroSection />
<FeaturedSection
featuredAgents={featuredAgents.agents as FeaturedAgent[]}
/>
<Separator />
<AgentsSection
sectionTitle="Top Agents"
agents={topAgents.agents as Agent[]}
/>
<Separator />
<FeaturedCreators
featuredCreators={featuredCreators.creators as FeaturedCreator[]}
/>
<Separator />
<BecomeACreator
title="Become a Creator"
description="Join our ever-growing community of hackers and tinkerers"
buttonText="Become a Creator"
/>
</main>
</div>
);
}

View File

@@ -0,0 +1,182 @@
"use client";
import { useState, useEffect } from "react";
import { AutoGPTServerAPI } from "@/lib/autogpt-server-api/client";
import { AgentsSection } from "@/components/agptui/composite/AgentsSection";
import { SearchBar } from "@/components/agptui/SearchBar";
import { FeaturedCreators } from "@/components/agptui/composite/FeaturedCreators";
import { Separator } from "@/components/ui/separator";
import { SearchFilterChips } from "@/components/agptui/SearchFilterChips";
import { SortDropdown } from "@/components/agptui/SortDropdown";
export default function Page({
searchParams,
}: {
searchParams: { searchTerm?: string; sort?: string };
}) {
return (
<SearchResults
searchTerm={searchParams.searchTerm || ""}
sort={searchParams.sort || "trending"}
/>
);
}
function SearchResults({
searchTerm,
sort,
}: {
searchTerm: string;
sort: string;
}) {
const [showAgents, setShowAgents] = useState(true);
const [showCreators, setShowCreators] = useState(true);
const [agents, setAgents] = useState<any[]>([]);
const [creators, setCreators] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
const api = new AutoGPTServerAPI();
try {
const [agentsRes, creatorsRes] = await Promise.all([
api.getStoreAgents({
search_query: searchTerm,
sorted_by: sort,
}),
api.getStoreCreators({
search_query: searchTerm,
}),
]);
setAgents(agentsRes.agents || []);
setCreators(creatorsRes.creators || []);
} catch (error) {
console.error("Error fetching data:", error);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [searchTerm, sort]);
const agentsCount = agents.length;
const creatorsCount = creators.length;
const totalCount = agentsCount + creatorsCount;
const handleFilterChange = (value: string) => {
if (value === "agents") {
setShowAgents(true);
setShowCreators(false);
} else if (value === "creators") {
setShowAgents(false);
setShowCreators(true);
} else {
setShowAgents(true);
setShowCreators(true);
}
};
const handleSortChange = (sortValue: string) => {
let sortBy = "recent";
if (sortValue === "runs") {
sortBy = "runs";
} else if (sortValue === "rating") {
sortBy = "rating";
}
const sortedAgents = [...agents].sort((a, b) => {
if (sortBy === "runs") {
return b.runs - a.runs;
} else if (sortBy === "rating") {
return b.rating - a.rating;
} else {
return (
new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
);
}
});
const sortedCreators = [...creators].sort((a, b) => {
if (sortBy === "runs") {
return b.agent_runs - a.agent_runs;
} else if (sortBy === "rating") {
return b.agent_rating - a.agent_rating;
} else {
// Creators don't have updated_at, sort by number of agents as fallback
return b.num_agents - a.num_agents;
}
});
setAgents(sortedAgents);
setCreators(sortedCreators);
};
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-loose text-neutral-800 dark:text-neutral-100">
{searchTerm}
</h1>
</div>
<div className="flex-none">
<SearchBar width="w-[439px]" height="h-[60px]" />
</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>
</div>
) : totalCount > 0 ? (
<>
<div className="mt-8 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-8">
<AgentsSection agents={agents} sectionTitle="Agents" />
</div>
)}
{showAgents && agentsCount > 0 && creatorsCount > 0 && (
<Separator />
)}
{showCreators && creatorsCount > 0 && (
<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>
);
}

View File

@@ -19,8 +19,8 @@ import {
NodeExecutionResult,
BlockUIType,
BlockCost,
useBackendAPI,
} from "@/lib/autogpt-server-api";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import {
beautifyString,
cn,
@@ -258,7 +258,7 @@ export function CustomNode({
) : (
propKey != "credentials" && (
<div className="flex gap-1">
<span className="text-m green mb-0 text-gray-900">
<span className="text-m green mb-0 text-gray-900 dark:text-gray-100">
{propSchema.title || beautifyString(propKey)}
</span>
<SchemaTooltip description={propSchema.description} />
@@ -460,49 +460,54 @@ export function CustomNode({
"custom-node",
"dark-theme",
"rounded-xl",
"bg-white/[.9]",
"border border-gray-300",
"bg-white/[.9] dark:bg-gray-800/[.9]",
"border border-gray-300 dark:border-gray-600",
data.uiType === BlockUIType.NOTE ? "w-[300px]" : "w-[500px]",
data.uiType === BlockUIType.NOTE ? "bg-yellow-100" : "bg-white",
data.uiType === BlockUIType.NOTE
? "bg-yellow-100 dark:bg-yellow-900"
: "bg-white dark:bg-gray-800",
selected ? "shadow-2xl" : "",
]
.filter(Boolean)
.join(" ");
const errorClass =
hasConfigErrors || hasOutputError ? "border-red-200 border-2" : "";
hasConfigErrors || hasOutputError
? "border-red-200 dark:border-red-800 border-2"
: "";
const statusClass = (() => {
if (hasConfigErrors || hasOutputError) return "border-red-200 border-4";
if (hasConfigErrors || hasOutputError)
return "border-red-200 dark:border-red-800 border-4";
switch (data.status?.toLowerCase()) {
case "completed":
return "border-green-200 border-4";
return "border-green-200 dark:border-green-800 border-4";
case "running":
return "border-yellow-200 border-4";
return "border-yellow-200 dark:border-yellow-800 border-4";
case "failed":
return "border-red-200 border-4";
return "border-red-200 dark:border-red-800 border-4";
case "incomplete":
return "border-purple-200 border-4";
return "border-purple-200 dark:border-purple-800 border-4";
case "queued":
return "border-cyan-200 border-4";
return "border-cyan-200 dark:border-cyan-800 border-4";
default:
return "";
}
})();
const statusBackgroundClass = (() => {
if (hasConfigErrors || hasOutputError) return "bg-red-200";
if (hasConfigErrors || hasOutputError) return "bg-red-200 dark:bg-red-800";
switch (data.status?.toLowerCase()) {
case "completed":
return "bg-green-200";
return "bg-green-200 dark:bg-green-800";
case "running":
return "bg-yellow-200";
return "bg-yellow-200 dark:bg-yellow-800";
case "failed":
return "bg-red-200";
return "bg-red-200 dark:bg-red-800";
case "incomplete":
return "bg-purple-200";
return "bg-purple-200 dark:bg-purple-800";
case "queued":
return "bg-cyan-200";
return "bg-cyan-200 dark:bg-cyan-800";
default:
return "";
}
@@ -591,36 +596,36 @@ export function CustomNode({
);
const LineSeparator = () => (
<div className="bg-white pt-6">
<Separator.Root className="h-[1px] w-full bg-gray-300"></Separator.Root>
<div className="bg-white pt-6 dark:bg-gray-800">
<Separator.Root className="h-[1px] w-full bg-gray-300 dark:bg-gray-600"></Separator.Root>
</div>
);
const ContextMenuContent = () => (
<ContextMenu.Content className="z-10 rounded-xl border bg-white p-1 shadow-md">
<ContextMenu.Content className="z-10 rounded-xl border bg-white p-1 shadow-md dark:bg-gray-800">
<ContextMenu.Item
onSelect={copyNode}
className="flex cursor-pointer items-center rounded-md px-3 py-2 hover:bg-gray-100"
className="flex cursor-pointer items-center rounded-md px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<CopyIcon className="mr-2 h-5 w-5" />
<span>Copy</span>
<CopyIcon className="mr-2 h-5 w-5 dark:text-gray-100" />
<span className="dark:text-gray-100">Copy</span>
</ContextMenu.Item>
{nodeFlowId && (
<ContextMenu.Item
onSelect={() => window.open(`/build?flowID=${nodeFlowId}`)}
className="flex cursor-pointer items-center rounded-md px-3 py-2 hover:bg-gray-100"
className="flex cursor-pointer items-center rounded-md px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
>
<ExitIcon className="mr-2 h-5 w-5" />
<span>Open agent</span>
<ExitIcon className="mr-2 h-5 w-5 dark:text-gray-100" />
<span className="dark:text-gray-100">Open agent</span>
</ContextMenu.Item>
)}
<ContextMenu.Separator className="my-1 h-px bg-gray-300" />
<ContextMenu.Separator className="my-1 h-px bg-gray-300 dark:bg-gray-600" />
<ContextMenu.Item
onSelect={deleteNode}
className="flex cursor-pointer items-center rounded-md px-3 py-2 text-red-500 hover:bg-gray-100"
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" />
<span>Delete</span>
<TrashIcon className="mr-2 h-5 w-5 text-red-500 dark:text-red-400" />
<span className="dark:text-red-400">Delete</span>
</ContextMenu.Item>
</ContextMenu.Content>
);

View File

@@ -70,9 +70,8 @@ export const FlowContext = createContext<FlowContextType | null>(null);
const FlowEditor: React.FC<{
flowID?: string;
template?: boolean;
className?: string;
}> = ({ flowID, template, className }) => {
}> = ({ flowID, className }) => {
const {
addNodes,
addEdges,
@@ -106,7 +105,7 @@ const FlowEditor: React.FC<{
setNodes,
edges,
setEdges,
} = useAgentGraph(flowID, template, visualizeBeads !== "no");
} = useAgentGraph(flowID, visualizeBeads !== "no");
const router = useRouter();
const pathname = usePathname();
@@ -661,9 +660,10 @@ const FlowEditor: React.FC<{
deleteKeyCode={["Backspace", "Delete"]}
minZoom={0.2}
maxZoom={2}
className="dark:bg-slate-900"
>
<Controls />
<Background />
<Background className="dark:bg-slate-800" />
<ControlPanel
className="absolute z-10"
controls={editorControls}

View File

@@ -35,7 +35,7 @@ const NodeHandle: FC<HandleProps> = ({
const label = (
<div className="flex flex-grow flex-row">
<span className="text-m green flex items-end pr-2 text-gray-900">
<span className="text-m green flex items-end pr-2 text-gray-900 dark:text-gray-100">
{title || schema.title || beautifyString(keyName.toLowerCase())}
{isRequired ? "*" : ""}
</span>
@@ -48,10 +48,10 @@ const NodeHandle: FC<HandleProps> = ({
const Dot = () => {
const color = isConnected
? getTypeBgColor(schema.type || "any")
: "border-gray-300";
: "border-gray-300 dark:border-gray-600";
return (
<div
className={`${color} m-1 h-4 w-4 rounded-full border-2 bg-white transition-colors duration-100 group-hover:bg-gray-300`}
className={`${color} m-1 h-4 w-4 rounded-full border-2 bg-white transition-colors duration-100 group-hover:bg-gray-300 dark:bg-slate-800 dark:group-hover:bg-gray-700`}
/>
);
};

View File

@@ -7,7 +7,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "./ui/button";
import { useSupabase } from "./SupabaseProvider";
import { useSupabase } from "./providers/SupabaseProvider";
import { useRouter } from "next/navigation";
import useUser from "@/hooks/useUser";

View File

@@ -14,15 +14,18 @@ const SchemaTooltip: React.FC<{ description?: string }> = ({ description }) => {
<TooltipProvider delayDuration={400}>
<Tooltip>
<TooltipTrigger asChild>
<Info className="rounded-full p-1 hover:bg-gray-300" size={24} />
<Info
className="rounded-full p-1 hover:bg-gray-300 dark:hover:bg-gray-700"
size={24}
/>
</TooltipTrigger>
<TooltipContent className="tooltip-content max-w-xs">
<TooltipContent className="tooltip-content max-w-xs bg-white text-gray-900 dark:bg-gray-800 dark:text-gray-100">
<ReactMarkdown
components={{
a: ({ node, ...props }) => (
<a
target="_blank"
className="text-blue-400 underline"
className="text-blue-400 underline dark:text-blue-300"
{...props}
/>
),

View File

@@ -1,12 +1,20 @@
"use client";
import React, { useEffect, useState } from "react";
import { Button } from "./ui/button";
import { QuestionMarkCircledIcon } from "@radix-ui/react-icons";
import { useRouter } from "next/navigation";
import { useRouter, usePathname } from "next/navigation";
const TallyPopupSimple = () => {
const [isFormVisible, setIsFormVisible] = useState(false);
const router = useRouter();
const pathname = usePathname();
const [show_tutorial, setShowTutorial] = useState(false);
useEffect(() => {
setShowTutorial(pathname.includes("build"));
}, [pathname]);
useEffect(() => {
// Load Tally script
@@ -49,21 +57,23 @@ const TallyPopupSimple = () => {
return (
<div className="fixed bottom-1 right-6 z-50 hidden select-none items-center gap-4 p-3 transition-all duration-300 ease-in-out md:flex">
{show_tutorial && (
<Button
variant="default"
onClick={resetTutorial}
className="font-inter mb-0 h-14 w-28 rounded-2xl bg-[rgba(65,65,64,1)] text-left text-lg font-medium leading-6"
>
Tutorial
</Button>
)}
<Button
variant="default"
onClick={resetTutorial}
className="font-inter mb-0 h-14 w-28 rounded-2xl bg-[rgba(65,65,64,1)] text-left text-lg font-medium leading-6"
>
Tutorial
</Button>
<Button
className="h-14 w-14 rounded-2xl bg-[rgba(65,65,64,1)]"
className="h-14 w-14 rounded-full bg-[rgba(65,65,64,1)]"
variant="default"
data-tally-open="3yx2L0"
data-tally-emoji-text="👋"
data-tally-emoji-animation="wave"
>
<QuestionMarkCircledIcon className="h-6 w-6" />
<QuestionMarkCircledIcon className="h-14 w-14" />
<span className="sr-only">Reach Out</span>
</Button>
</div>

View File

@@ -1,149 +1,149 @@
"use client";
// "use client";
import {
Dialog,
DialogContent,
DialogClose,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorInput,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from "@/components/ui/multiselect";
import { Controller, useForm } from "react-hook-form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useState } from "react";
import { addFeaturedAgent } from "./actions";
import { Agent } from "@/lib/marketplace-api/types";
// import {
// Dialog,
// DialogContent,
// DialogClose,
// DialogFooter,
// DialogHeader,
// DialogTitle,
// DialogTrigger,
// } from "@/components/ui/dialog";
// import { Button } from "@/components/ui/button";
// import {
// MultiSelector,
// MultiSelectorContent,
// MultiSelectorInput,
// MultiSelectorItem,
// MultiSelectorList,
// MultiSelectorTrigger,
// } from "@/components/ui/multiselect";
// import { Controller, useForm } from "react-hook-form";
// import {
// Select,
// SelectContent,
// SelectItem,
// SelectTrigger,
// SelectValue,
// } from "@/components/ui/select";
// import { useState } from "react";
// import { addFeaturedAgent } from "./actions";
// import { Agent } from "@/lib/marketplace-api/types";
type FormData = {
agent: string;
categories: string[];
};
// type FormData = {
// agent: string;
// categories: string[];
// };
export const AdminAddFeaturedAgentDialog = ({
categories,
agents,
}: {
categories: string[];
agents: Agent[];
}) => {
const [selectedAgent, setSelectedAgent] = useState<string>("");
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
// export const AdminAddFeaturedAgentDialog = ({
// categories,
// agents,
// }: {
// categories: string[];
// agents: Agent[];
// }) => {
// const [selectedAgent, setSelectedAgent] = useState<string>("");
// const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const {
control,
handleSubmit,
watch,
setValue,
formState: { errors },
} = useForm<FormData>({
defaultValues: {
agent: "",
categories: [],
},
});
// const {
// control,
// handleSubmit,
// watch,
// setValue,
// formState: { errors },
// } = useForm<FormData>({
// defaultValues: {
// agent: "",
// categories: [],
// },
// });
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="sm">
Add Featured Agent
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add Featured Agent</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4">
<Controller
name="agent"
control={control}
rules={{ required: true }}
render={({ field }) => (
<div>
<label htmlFor={field.name}>Agent</label>
<Select
onValueChange={(value) => {
field.onChange(value);
setSelectedAgent(value);
}}
value={field.value || ""}
>
<SelectTrigger>
<SelectValue placeholder="Select an agent" />
</SelectTrigger>
<SelectContent>
{/* Populate with agents */}
{agents.map((agent) => (
<SelectItem key={agent.id} value={agent.id}>
{agent.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
/>
<Controller
name="categories"
control={control}
render={({ field }) => (
<MultiSelector
values={field.value || []}
onValuesChange={(values) => {
field.onChange(values);
setSelectedCategories(values);
}}
>
<MultiSelectorTrigger>
<MultiSelectorInput placeholder="Select categories" />
</MultiSelectorTrigger>
<MultiSelectorContent>
<MultiSelectorList>
{categories.map((category) => (
<MultiSelectorItem key={category} value={category}>
{category}
</MultiSelectorItem>
))}
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
)}
/>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button
type="submit"
onClick={async () => {
// Handle adding the featured agent
await addFeaturedAgent(selectedAgent, selectedCategories);
// close the dialog
}}
>
Add
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
// return (
// <Dialog>
// <DialogTrigger asChild>
// <Button variant="outline" size="sm">
// Add Featured Agent
// </Button>
// </DialogTrigger>
// <DialogContent>
// <DialogHeader>
// <DialogTitle>Add Featured Agent</DialogTitle>
// </DialogHeader>
// <div className="flex flex-col gap-4">
// <Controller
// name="agent"
// control={control}
// rules={{ required: true }}
// render={({ field }) => (
// <div>
// <label htmlFor={field.name}>Agent</label>
// <Select
// onValueChange={(value) => {
// field.onChange(value);
// setSelectedAgent(value);
// }}
// value={field.value || ""}
// >
// <SelectTrigger>
// <SelectValue placeholder="Select an agent" />
// </SelectTrigger>
// <SelectContent>
// {/* Populate with agents */}
// {agents.map((agent) => (
// <SelectItem key={agent.id} value={agent.id}>
// {agent.name}
// </SelectItem>
// ))}
// </SelectContent>
// </Select>
// </div>
// )}
// />
// <Controller
// name="categories"
// control={control}
// render={({ field }) => (
// <MultiSelector
// values={field.value || []}
// onValuesChange={(values) => {
// field.onChange(values);
// setSelectedCategories(values);
// }}
// >
// <MultiSelectorTrigger>
// <MultiSelectorInput placeholder="Select categories" />
// </MultiSelectorTrigger>
// <MultiSelectorContent>
// <MultiSelectorList>
// {categories.map((category) => (
// <MultiSelectorItem key={category} value={category}>
// {category}
// </MultiSelectorItem>
// ))}
// </MultiSelectorList>
// </MultiSelectorContent>
// </MultiSelector>
// )}
// />
// </div>
// <DialogFooter>
// <DialogClose asChild>
// <Button variant="outline">Cancel</Button>
// </DialogClose>
// <DialogClose asChild>
// <Button
// type="submit"
// onClick={async () => {
// // Handle adding the featured agent
// await addFeaturedAgent(selectedAgent, selectedCategories);
// // close the dialog
// }}
// >
// Add
// </Button>
// </DialogClose>
// </DialogFooter>
// </DialogContent>
// </Dialog>
// );
// };

View File

@@ -1,74 +1,74 @@
import { Button } from "@/components/ui/button";
import {
getFeaturedAgents,
removeFeaturedAgent,
getCategories,
getNotFeaturedAgents,
} from "./actions";
// import { Button } from "@/components/ui/button";
// import {
// getFeaturedAgents,
// removeFeaturedAgent,
// getCategories,
// getNotFeaturedAgents,
// } from "./actions";
import FeaturedAgentsTable from "./FeaturedAgentsTable";
import { AdminAddFeaturedAgentDialog } from "./AdminAddFeaturedAgentDialog";
import { revalidatePath } from "next/cache";
import * as Sentry from "@sentry/nextjs";
// import FeaturedAgentsTable from "./FeaturedAgentsTable";
// import { AdminAddFeaturedAgentDialog } from "./AdminAddFeaturedAgentDialog";
// import { revalidatePath } from "next/cache";
// import * as Sentry from "@sentry/nextjs";
export default async function AdminFeaturedAgentsControl({
className,
}: {
className?: string;
}) {
// add featured agent button
// modal to select agent?
// modal to select categories?
// table of featured agents
// in table
// remove featured agent button
// edit featured agent categories button
// table footer
// Next page button
// Previous page button
// Page number input
// Page size input
// Total pages input
// Go to page button
// export default async function AdminFeaturedAgentsControl({
// className,
// }: {
// className?: string;
// }) {
// // add featured agent button
// // modal to select agent?
// // modal to select categories?
// // table of featured agents
// // in table
// // remove featured agent button
// // edit featured agent categories button
// // table footer
// // Next page button
// // Previous page button
// // Page number input
// // Page size input
// // Total pages input
// // Go to page button
const page = 1;
const pageSize = 10;
// const page = 1;
// const pageSize = 10;
const agents = await getFeaturedAgents(page, pageSize);
// const agents = await getFeaturedAgents(page, pageSize);
const categories = await getCategories();
// const categories = await getCategories();
const notFeaturedAgents = await getNotFeaturedAgents();
// const notFeaturedAgents = await getNotFeaturedAgents();
return (
<div className={`flex flex-col gap-4 ${className}`}>
<div className="mb-4 flex justify-between">
<h3 className="text-lg font-semibold">Featured Agent Controls</h3>
<AdminAddFeaturedAgentDialog
categories={categories.unique_categories}
agents={notFeaturedAgents.items}
/>
</div>
<FeaturedAgentsTable
agents={agents.items}
globalActions={[
{
component: <Button>Remove</Button>,
action: async (rows) => {
"use server";
return await Sentry.withServerActionInstrumentation(
"removeFeaturedAgent",
{},
async () => {
const all = rows.map((row) => removeFeaturedAgent(row.id));
await Promise.all(all);
revalidatePath("/marketplace");
},
);
},
},
]}
/>
</div>
);
}
// return (
// <div className={`flex flex-col gap-4 ${className}`}>
// <div className="mb-4 flex justify-between">
// <h3 className="text-lg font-semibold">Featured Agent Controls</h3>
// <AdminAddFeaturedAgentDialog
// categories={categories.unique_categories}
// agents={notFeaturedAgents.items}
// />
// </div>
// <FeaturedAgentsTable
// agents={agents.items}
// globalActions={[
// {
// component: <Button>Remove</Button>,
// action: async (rows) => {
// "use server";
// return await Sentry.withServerActionInstrumentation(
// "removeFeaturedAgent",
// {},
// async () => {
// const all = rows.map((row) => removeFeaturedAgent(row.id));
// await Promise.all(all);
// revalidatePath("/marketplace");
// },
// );
// },
// },
// ]}
// />
// </div>
// );
// }

View File

@@ -1,36 +1,36 @@
import { Agent } from "@/lib/marketplace-api";
import AdminMarketplaceCard from "./AdminMarketplaceCard";
import { ClipboardX } from "lucide-react";
// import { Agent } from "@/lib/marketplace-api";
// import AdminMarketplaceCard from "./AdminMarketplaceCard";
// import { ClipboardX } from "lucide-react";
export default function AdminMarketplaceAgentList({
agents,
className,
}: {
agents: Agent[];
className?: string;
}) {
if (agents.length === 0) {
return (
<div className={className}>
<h3 className="text-lg font-semibold">Agents to review</h3>
<div className="flex flex-col items-center justify-center py-12 text-gray-500">
<ClipboardX size={48} />
<p className="mt-4 text-lg font-semibold">No agents to review</p>
</div>
</div>
);
}
// export default function AdminMarketplaceAgentList({
// agents,
// className,
// }: {
// agents: Agent[];
// className?: string;
// }) {
// if (agents.length === 0) {
// return (
// <div className={className}>
// <h3 className="text-lg font-semibold">Agents to review</h3>
// <div className="flex flex-col items-center justify-center py-12 text-gray-500">
// <ClipboardX size={48} />
// <p className="mt-4 text-lg font-semibold">No agents to review</p>
// </div>
// </div>
// );
// }
return (
<div className={`flex flex-col gap-4 ${className}`}>
<div>
<h3 className="text-lg font-semibold">Agents to review</h3>
</div>
<div className="flex flex-col gap-4">
{agents.map((agent) => (
<AdminMarketplaceCard agent={agent} key={agent.id} />
))}
</div>
</div>
);
}
// return (
// <div className={`flex flex-col gap-4 ${className}`}>
// <div>
// <h3 className="text-lg font-semibold">Agents to review</h3>
// </div>
// <div className="flex flex-col gap-4">
// {agents.map((agent) => (
// <AdminMarketplaceCard agent={agent} key={agent.id} />
// ))}
// </div>
// </div>
// );
// }

View File

@@ -1,113 +1,113 @@
"use client";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { approveAgent, rejectAgent } from "./actions";
import { Agent } from "@/lib/marketplace-api";
import Link from "next/link";
import { useState } from "react";
import { Input } from "@/components/ui/input";
// "use client";
// import { Card } from "@/components/ui/card";
// import { Button } from "@/components/ui/button";
// import { Badge } from "@/components/ui/badge";
// import { ScrollArea } from "@/components/ui/scroll-area";
// import { approveAgent, rejectAgent } from "./actions";
// import { Agent } from "@/lib/marketplace-api";
// import Link from "next/link";
// import { useState } from "react";
// import { Input } from "@/components/ui/input";
function AdminMarketplaceCard({ agent }: { agent: Agent }) {
const [isApproved, setIsApproved] = useState(false);
const [isRejected, setIsRejected] = useState(false);
const [comment, setComment] = useState("");
// function AdminMarketplaceCard({ agent }: { agent: Agent }) {
// const [isApproved, setIsApproved] = useState(false);
// const [isRejected, setIsRejected] = useState(false);
// const [comment, setComment] = useState("");
const approveAgentWithId = approveAgent.bind(
null,
agent.id,
agent.version,
comment,
);
const rejectAgentWithId = rejectAgent.bind(
null,
agent.id,
agent.version,
comment,
);
// const approveAgentWithId = approveAgent.bind(
// null,
// agent.id,
// agent.version,
// comment,
// );
// const rejectAgentWithId = rejectAgent.bind(
// null,
// agent.id,
// agent.version,
// comment,
// );
const handleApprove = async (e: React.FormEvent) => {
e.preventDefault();
await approveAgentWithId();
setIsApproved(true);
};
// const handleApprove = async (e: React.FormEvent) => {
// e.preventDefault();
// await approveAgentWithId();
// setIsApproved(true);
// };
const handleReject = async (e: React.FormEvent) => {
e.preventDefault();
await rejectAgentWithId();
setIsRejected(true);
};
// const handleReject = async (e: React.FormEvent) => {
// e.preventDefault();
// await rejectAgentWithId();
// setIsRejected(true);
// };
return (
<>
{!isApproved && !isRejected && (
<Card key={agent.id} className="m-3 flex h-[300px] flex-col p-4">
<div className="mb-2 flex items-start justify-between">
<Link
href={`/marketplace/${agent.id}`}
className="text-lg font-semibold hover:underline"
>
{agent.name}
</Link>
<Badge variant="outline">v{agent.version}</Badge>
</div>
<p className="mb-2 text-sm text-gray-500">by {agent.author}</p>
<ScrollArea className="flex-grow">
<p className="mb-2 text-sm text-gray-600">{agent.description}</p>
<div className="mb-2 flex flex-wrap gap-1">
{agent.categories.map((category) => (
<Badge key={category} variant="secondary">
{category}
</Badge>
))}
</div>
<div className="flex flex-wrap gap-1">
{agent.keywords.map((keyword) => (
<Badge key={keyword} variant="outline">
{keyword}
</Badge>
))}
</div>
</ScrollArea>
<div className="mb-2 flex justify-between text-xs text-gray-500">
<span>
Created: {new Date(agent.createdAt).toLocaleDateString()}
</span>
<span>
Updated: {new Date(agent.updatedAt).toLocaleDateString()}
</span>
</div>
<div className="mb-4 flex justify-between text-sm">
<span>👁 {agent.views}</span>
<span> {agent.downloads}</span>
</div>
<div className="mt-auto space-y-2">
<div className="flex justify-end space-x-2">
<Input
type="text"
placeholder="Add a comment (optional)"
value={comment}
onChange={(e) => setComment(e.target.value)}
/>
{!isRejected && (
<form onSubmit={handleReject}>
<Button variant="outline" type="submit">
Reject
</Button>
</form>
)}
{!isApproved && (
<form onSubmit={handleApprove}>
<Button type="submit">Approve</Button>
</form>
)}
</div>
</div>
</Card>
)}
</>
);
}
// return (
// <>
// {!isApproved && !isRejected && (
// <Card key={agent.id} className="m-3 flex h-[300px] flex-col p-4">
// <div className="mb-2 flex items-start justify-between">
// <Link
// href={`/marketplace/${agent.id}`}
// className="text-lg font-semibold hover:underline"
// >
// {agent.name}
// </Link>
// <Badge variant="outline">v{agent.version}</Badge>
// </div>
// <p className="mb-2 text-sm text-gray-500">by {agent.author}</p>
// <ScrollArea className="flex-grow">
// <p className="mb-2 text-sm text-gray-600">{agent.description}</p>
// <div className="mb-2 flex flex-wrap gap-1">
// {agent.categories.map((category) => (
// <Badge key={category} variant="secondary">
// {category}
// </Badge>
// ))}
// </div>
// <div className="flex flex-wrap gap-1">
// {agent.keywords.map((keyword) => (
// <Badge key={keyword} variant="outline">
// {keyword}
// </Badge>
// ))}
// </div>
// </ScrollArea>
// <div className="mb-2 flex justify-between text-xs text-gray-500">
// <span>
// Created: {new Date(agent.createdAt).toLocaleDateString()}
// </span>
// <span>
// Updated: {new Date(agent.updatedAt).toLocaleDateString()}
// </span>
// </div>
// <div className="mb-4 flex justify-between text-sm">
// <span>👁 {agent.views}</span>
// <span>⬇️ {agent.downloads}</span>
// </div>
// <div className="mt-auto space-y-2">
// <div className="flex justify-end space-x-2">
// <Input
// type="text"
// placeholder="Add a comment (optional)"
// value={comment}
// onChange={(e) => setComment(e.target.value)}
// />
// {!isRejected && (
// <form onSubmit={handleReject}>
// <Button variant="outline" type="submit">
// Reject
// </Button>
// </form>
// )}
// {!isApproved && (
// <form onSubmit={handleApprove}>
// <Button type="submit">Approve</Button>
// </form>
// )}
// </div>
// </div>
// </Card>
// )}
// </>
// );
// }
export default AdminMarketplaceCard;
// export default AdminMarketplaceCard;

View File

@@ -1,114 +1,114 @@
"use client";
// "use client";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { DataTable } from "@/components/ui/data-table";
import { Agent } from "@/lib/marketplace-api";
import { ColumnDef } from "@tanstack/react-table";
import { ArrowUpDown } from "lucide-react";
import { removeFeaturedAgent } from "./actions";
import { GlobalActions } from "@/components/ui/data-table";
// import { Button } from "@/components/ui/button";
// import { Checkbox } from "@/components/ui/checkbox";
// import { DataTable } from "@/components/ui/data-table";
// import { Agent } from "@/lib/marketplace-api";
// import { ColumnDef } from "@tanstack/react-table";
// import { ArrowUpDown } from "lucide-react";
// import { removeFeaturedAgent } from "./actions";
// import { GlobalActions } from "@/components/ui/data-table";
export const columns: ColumnDef<Agent>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
},
{
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Name
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
accessorKey: "name",
},
{
header: "Description",
accessorKey: "description",
},
{
header: "Categories",
accessorKey: "categories",
},
{
header: "Keywords",
accessorKey: "keywords",
},
{
header: "Downloads",
accessorKey: "downloads",
},
{
header: "Author",
accessorKey: "author",
},
{
header: "Version",
accessorKey: "version",
},
{
header: "actions",
cell: ({ row }) => {
const handleRemove = async () => {
await removeFeaturedAgentWithId();
};
// const handleEdit = async () => {
// console.log("edit");
// };
const removeFeaturedAgentWithId = removeFeaturedAgent.bind(
null,
row.original.id,
);
return (
<div className="flex justify-end gap-2">
<Button variant="outline" size="sm" onClick={handleRemove}>
Remove
</Button>
{/* <Button variant="outline" size="sm" onClick={handleEdit}>
Edit
</Button> */}
</div>
);
},
},
];
// export const columns: ColumnDef<Agent>[] = [
// {
// id: "select",
// header: ({ table }) => (
// <Checkbox
// checked={
// table.getIsAllPageRowsSelected() ||
// (table.getIsSomePageRowsSelected() && "indeterminate")
// }
// onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
// aria-label="Select all"
// />
// ),
// cell: ({ row }) => (
// <Checkbox
// checked={row.getIsSelected()}
// onCheckedChange={(value) => row.toggleSelected(!!value)}
// aria-label="Select row"
// />
// ),
// },
// {
// header: ({ column }) => {
// return (
// <Button
// variant="ghost"
// onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
// >
// Name
// <ArrowUpDown className="ml-2 h-4 w-4" />
// </Button>
// );
// },
// accessorKey: "name",
// },
// {
// header: "Description",
// accessorKey: "description",
// },
// {
// header: "Categories",
// accessorKey: "categories",
// },
// {
// header: "Keywords",
// accessorKey: "keywords",
// },
// {
// header: "Downloads",
// accessorKey: "downloads",
// },
// {
// header: "Author",
// accessorKey: "author",
// },
// {
// header: "Version",
// accessorKey: "version",
// },
// {
// header: "actions",
// cell: ({ row }) => {
// const handleRemove = async () => {
// await removeFeaturedAgentWithId();
// };
// // const handleEdit = async () => {
// // console.log("edit");
// // };
// const removeFeaturedAgentWithId = removeFeaturedAgent.bind(
// null,
// row.original.id,
// );
// return (
// <div className="flex justify-end gap-2">
// <Button variant="outline" size="sm" onClick={handleRemove}>
// Remove
// </Button>
// {/* <Button variant="outline" size="sm" onClick={handleEdit}>
// Edit
// </Button> */}
// </div>
// );
// },
// },
// ];
export default function FeaturedAgentsTable({
agents,
globalActions,
}: {
agents: Agent[];
globalActions: GlobalActions<Agent>[];
}) {
return (
<DataTable
columns={columns}
data={agents}
filterPlaceholder="Search agents..."
filterColumn="name"
globalActions={globalActions}
/>
);
}
// export default function FeaturedAgentsTable({
// agents,
// globalActions,
// }: {
// agents: Agent[];
// globalActions: GlobalActions<Agent>[];
// }) {
// return (
// <DataTable
// columns={columns}
// data={agents}
// filterPlaceholder="Search agents..."
// filterColumn="name"
// globalActions={globalActions}
// />
// );
// }

View File

@@ -1,155 +1,155 @@
"use server";
import MarketplaceAPI from "@/lib/marketplace-api";
import ServerSideMarketplaceAPI from "@/lib/marketplace-api/server-client";
import { revalidatePath } from "next/cache";
import * as Sentry from "@sentry/nextjs";
import { checkAuth, createServerClient } from "@/lib/supabase/server";
import { redirect } from "next/navigation";
import { createClient } from "@/lib/supabase/client";
// "use server";
// import MarketplaceAPI from "@/lib/marketplace-api";
// import ServerSideMarketplaceAPI from "@/lib/marketplace-api/server-client";
// import { revalidatePath } from "next/cache";
// import * as Sentry from "@sentry/nextjs";
// import { checkAuth, createServerClient } from "@/lib/supabase/server";
// import { redirect } from "next/navigation";
// import { createClient } from "@/lib/supabase/client";
export async function approveAgent(
agentId: string,
version: number,
comment: string,
) {
return await Sentry.withServerActionInstrumentation(
"approveAgent",
{},
async () => {
await checkAuth();
// export async function approveAgent(
// agentId: string,
// version: number,
// comment: string,
// ) {
// return await Sentry.withServerActionInstrumentation(
// "approveAgent",
// {},
// async () => {
// await checkAuth();
const api = new ServerSideMarketplaceAPI();
await api.approveAgentSubmission(agentId, version, comment);
console.debug(`Approving agent ${agentId}`);
revalidatePath("/marketplace");
},
);
}
// const api = new ServerSideMarketplaceAPI();
// await api.approveAgentSubmission(agentId, version, comment);
// console.debug(`Approving agent ${agentId}`);
// revalidatePath("/marketplace");
// },
// );
// }
export async function rejectAgent(
agentId: string,
version: number,
comment: string,
) {
return await Sentry.withServerActionInstrumentation(
"rejectAgent",
{},
async () => {
await checkAuth();
const api = new ServerSideMarketplaceAPI();
await api.rejectAgentSubmission(agentId, version, comment);
console.debug(`Rejecting agent ${agentId}`);
revalidatePath("/marketplace");
},
);
}
// export async function rejectAgent(
// agentId: string,
// version: number,
// comment: string,
// ) {
// return await Sentry.withServerActionInstrumentation(
// "rejectAgent",
// {},
// async () => {
// await checkAuth();
// const api = new ServerSideMarketplaceAPI();
// await api.rejectAgentSubmission(agentId, version, comment);
// console.debug(`Rejecting agent ${agentId}`);
// revalidatePath("/marketplace");
// },
// );
// }
export async function getReviewableAgents() {
return await Sentry.withServerActionInstrumentation(
"getReviewableAgents",
{},
async () => {
await checkAuth();
const api = new ServerSideMarketplaceAPI();
return api.getAgentSubmissions();
},
);
}
// export async function getReviewableAgents() {
// return await Sentry.withServerActionInstrumentation(
// "getReviewableAgents",
// {},
// async () => {
// await checkAuth();
// const api = new ServerSideMarketplaceAPI();
// return api.getAgentSubmissions();
// },
// );
// }
export async function getFeaturedAgents(
page: number = 1,
pageSize: number = 10,
) {
return await Sentry.withServerActionInstrumentation(
"getFeaturedAgents",
{},
async () => {
await checkAuth();
const api = new ServerSideMarketplaceAPI();
const featured = await api.getFeaturedAgents(page, pageSize);
console.debug(`Getting featured agents ${featured.items.length}`);
return featured;
},
);
}
// export async function getFeaturedAgents(
// page: number = 1,
// pageSize: number = 10,
// ) {
// return await Sentry.withServerActionInstrumentation(
// "getFeaturedAgents",
// {},
// async () => {
// await checkAuth();
// const api = new ServerSideMarketplaceAPI();
// const featured = await api.getFeaturedAgents(page, pageSize);
// console.debug(`Getting featured agents ${featured.items.length}`);
// return featured;
// },
// );
// }
export async function getFeaturedAgent(agentId: string) {
return await Sentry.withServerActionInstrumentation(
"getFeaturedAgent",
{},
async () => {
await checkAuth();
const api = new ServerSideMarketplaceAPI();
const featured = await api.getFeaturedAgent(agentId);
console.debug(`Getting featured agent ${featured.agentId}`);
return featured;
},
);
}
// export async function getFeaturedAgent(agentId: string) {
// return await Sentry.withServerActionInstrumentation(
// "getFeaturedAgent",
// {},
// async () => {
// await checkAuth();
// const api = new ServerSideMarketplaceAPI();
// const featured = await api.getFeaturedAgent(agentId);
// console.debug(`Getting featured agent ${featured.agentId}`);
// return featured;
// },
// );
// }
export async function addFeaturedAgent(
agentId: string,
categories: string[] = ["featured"],
) {
return await Sentry.withServerActionInstrumentation(
"addFeaturedAgent",
{},
async () => {
await checkAuth();
const api = new ServerSideMarketplaceAPI();
await api.addFeaturedAgent(agentId, categories);
console.debug(`Adding featured agent ${agentId}`);
revalidatePath("/marketplace");
},
);
}
// export async function addFeaturedAgent(
// agentId: string,
// categories: string[] = ["featured"],
// ) {
// return await Sentry.withServerActionInstrumentation(
// "addFeaturedAgent",
// {},
// async () => {
// await checkAuth();
// const api = new ServerSideMarketplaceAPI();
// await api.addFeaturedAgent(agentId, categories);
// console.debug(`Adding featured agent ${agentId}`);
// revalidatePath("/marketplace");
// },
// );
// }
export async function removeFeaturedAgent(
agentId: string,
categories: string[] = ["featured"],
) {
return await Sentry.withServerActionInstrumentation(
"removeFeaturedAgent",
{},
async () => {
await checkAuth();
const api = new ServerSideMarketplaceAPI();
await api.removeFeaturedAgent(agentId, categories);
console.debug(`Removing featured agent ${agentId}`);
revalidatePath("/marketplace");
},
);
}
// export async function removeFeaturedAgent(
// agentId: string,
// categories: string[] = ["featured"],
// ) {
// return await Sentry.withServerActionInstrumentation(
// "removeFeaturedAgent",
// {},
// async () => {
// await checkAuth();
// const api = new ServerSideMarketplaceAPI();
// await api.removeFeaturedAgent(agentId, categories);
// console.debug(`Removing featured agent ${agentId}`);
// revalidatePath("/marketplace");
// },
// );
// }
export async function getCategories() {
return await Sentry.withServerActionInstrumentation(
"getCategories",
{},
async () => {
await checkAuth();
const api = new ServerSideMarketplaceAPI();
const categories = await api.getCategories();
console.debug(
`Getting categories ${categories.unique_categories.length}`,
);
return categories;
},
);
}
// export async function getCategories() {
// return await Sentry.withServerActionInstrumentation(
// "getCategories",
// {},
// async () => {
// await checkAuth();
// const api = new ServerSideMarketplaceAPI();
// const categories = await api.getCategories();
// console.debug(
// `Getting categories ${categories.unique_categories.length}`,
// );
// return categories;
// },
// );
// }
export async function getNotFeaturedAgents(
page: number = 1,
pageSize: number = 100,
) {
return await Sentry.withServerActionInstrumentation(
"getNotFeaturedAgents",
{},
async () => {
await checkAuth();
const api = new ServerSideMarketplaceAPI();
const agents = await api.getNotFeaturedAgents(page, pageSize);
console.debug(`Getting not featured agents ${agents.items.length}`);
return agents;
},
);
}
// export async function getNotFeaturedAgents(
// page: number = 1,
// pageSize: number = 100,
// ) {
// return await Sentry.withServerActionInstrumentation(
// "getNotFeaturedAgents",
// {},
// async () => {
// await checkAuth();
// const api = new ServerSideMarketplaceAPI();
// const agents = await api.getNotFeaturedAgents(page, pageSize);
// console.debug(`Getting not featured agents ${agents.items.length}`);
// return agents;
// },
// );
// }

View File

@@ -96,19 +96,16 @@ export const AgentImportForm: React.FC<
name: values.agentName,
description: values.agentDescription,
is_active: !values.importAsTemplate,
is_template: values.importAsTemplate,
};
(values.importAsTemplate
? api.createTemplate(payload)
: api.createGraph(payload)
)
api
.createGraph(payload)
.then((response) => {
const qID = values.importAsTemplate ? "templateID" : "flowID";
const qID = "flowID";
window.location.href = `/build?${qID}=${response.id}`;
})
.catch((error) => {
const entity_type = values.importAsTemplate ? "template" : "agent";
const entity_type = "agent";
form.setError("root", {
message: `Could not create ${entity_type}: ${error}`,
});
@@ -159,7 +156,6 @@ export const AgentImportForm: React.FC<
setAgentObject(agent);
form.setValue("agentName", agent.name);
form.setValue("agentDescription", agent.description);
form.setValue("importAsTemplate", agent.is_template);
} catch (error) {
console.error("Error loading agent file:", error);
}
@@ -202,41 +198,6 @@ export const AgentImportForm: React.FC<
</FormItem>
)}
/>
<FormField
control={form.control}
name="importAsTemplate"
disabled={!agentObject}
render={({ field }) => (
<FormItem>
<FormLabel>Import as</FormLabel>
<FormControl>
<div className="flex items-center space-x-2">
<span
className={
field.value ? "text-gray-400 dark:text-gray-600" : ""
}
>
Agent
</span>
<Switch
data-testid="import-as-template-switch"
disabled={field.disabled}
checked={field.value}
onCheckedChange={field.onChange}
/>
<span
className={
field.value ? "" : "text-gray-400 dark:text-gray-600"
}
>
Template
</span>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="w-full"

View File

@@ -0,0 +1,115 @@
import * as React from "react";
import Image from "next/image";
import { PlayIcon } from "@radix-ui/react-icons";
import { Button } from "./Button";
const isValidVideoFile = (url: string): boolean => {
const videoExtensions = /\.(mp4|webm|ogg)$/i;
return videoExtensions.test(url);
};
const isValidVideoUrl = (url: string): boolean => {
const videoExtensions = /\.(mp4|webm|ogg)$/i;
const youtubeRegex = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/;
return videoExtensions.test(url) || youtubeRegex.test(url);
};
const getYouTubeVideoId = (url: string) => {
const regExp =
/^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
const match = url.match(regExp);
return match && match[7].length === 11 ? match[7] : null;
};
interface AgentImageItemProps {
image: string;
index: number;
playingVideoIndex: number | null;
handlePlay: (index: number) => void;
handlePause: (index: number) => void;
}
export const AgentImageItem: React.FC<AgentImageItemProps> = React.memo(
({ image, index, playingVideoIndex, handlePlay, handlePause }) => {
const videoRef = React.useRef<HTMLVideoElement>(null);
React.useEffect(() => {
if (
playingVideoIndex !== index &&
videoRef.current &&
!videoRef.current.paused
) {
videoRef.current.pause();
}
}, [playingVideoIndex, index]);
const isVideoFile = isValidVideoFile(image);
return (
<div className="relative">
<div className="h-[15rem] overflow-hidden rounded-xl bg-[#a8a8a8] dark:bg-neutral-700 sm:h-[20rem] sm:w-full md:h-[25rem] lg:h-[30rem]">
{isValidVideoUrl(image) ? (
getYouTubeVideoId(image) ? (
<iframe
width="100%"
height="100%"
src={`https://www.youtube.com/embed/${getYouTubeVideoId(image)}`}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
title="YouTube video player"
></iframe>
) : (
<div className="relative h-full w-full overflow-hidden">
<video
ref={videoRef}
className="absolute inset-0 h-full w-full object-cover"
controls
preload="metadata"
poster={`${image}#t=0.1`}
style={{ objectPosition: "center 25%" }}
onPlay={() => handlePlay(index)}
onPause={() => handlePause(index)}
autoPlay={false}
title="Video"
>
<source src={image} type="video/mp4" />
Your browser does not support the video tag.
</video>
</div>
)
) : (
<div className="relative h-full w-full">
<Image
src={image}
alt="Image"
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="rounded-xl 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
variant="default"
size="default"
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>
</div>
)}
</div>
);
},
);
AgentImageItem.displayName = "AgentImageItem";

View File

@@ -0,0 +1,58 @@
import type { Meta, StoryObj } from "@storybook/react";
import { AgentImages } from "./AgentImages";
const meta = {
title: "AGPT UI/Agent Images",
component: AgentImages,
parameters: {
layout: {
center: true,
fullscreen: true,
padding: 0,
},
},
tags: ["autodocs"],
argTypes: {
images: { control: "object" },
},
} satisfies Meta<typeof AgentImages>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
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

@@ -0,0 +1,44 @@
"use client";
import * as React from "react";
import { AgentImageItem } from "./AgentImageItem";
interface AgentImagesProps {
images: string[];
}
export const AgentImages: React.FC<AgentImagesProps> = ({ images }) => {
const [playingVideoIndex, setPlayingVideoIndex] = React.useState<
number | null
>(null);
const handlePlay = React.useCallback((index: number) => {
setPlayingVideoIndex(index);
}, []);
const handlePause = React.useCallback(
(index: number) => {
if (playingVideoIndex === index) {
setPlayingVideoIndex(null);
}
},
[playingVideoIndex],
);
return (
<div className="w-full overflow-y-auto bg-white px-2 dark:bg-gray-800 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>
);
};

View File

@@ -0,0 +1,136 @@
import type { Meta, StoryObj } from "@storybook/react";
import { AgentInfo } from "./AgentInfo";
import { userEvent, within } from "@storybook/test";
const meta = {
title: "AGPT UI/Agent Info",
component: AgentInfo,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {
onRunAgent: { action: "run agent clicked" },
name: { control: "text" },
creator: { control: "text" },
shortDescription: { control: "text" },
longDescription: { control: "text" },
rating: { control: "number", min: 0, max: 5, step: 0.1 },
runs: { control: "number" },
categories: { control: "object" },
lastUpdated: { control: "text" },
version: { control: "text" },
},
} satisfies Meta<typeof AgentInfo>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
onRunAgent: () => console.log("Run agent clicked"),
name: "AI Video Generator",
creator: "Toran Richards",
shortDescription:
"Transform ideas into breathtaking images with this AI-powered Image 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:
- Customizable video output
- 15+ pre-made templates
- Auto scene detection
- Smart text-to-speech
- Multiple export formats
- SEO-optimized suggestions`,
rating: 4.7,
runs: 1500,
categories: ["Video", "Content Creation", "Social Media"],
lastUpdated: "2 days ago",
version: "1.2.0",
},
};
export const LowRating: 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",
},
};
export const HighRuns: 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",
},
};

View File

@@ -0,0 +1,120 @@
"use client";
import * as React from "react";
import { IconPlay, IconStar, StarRatingIcons } from "@/components/ui/icons";
import Link from "next/link";
import { Separator } from "@/components/ui/separator";
interface AgentInfoProps {
name: string;
creator: string;
shortDescription: string;
longDescription: string;
rating: number;
runs: number;
categories: string[];
lastUpdated: string;
version: string;
}
export const AgentInfo: React.FC<AgentInfoProps> = ({
name,
creator,
shortDescription,
longDescription,
rating,
runs,
categories,
lastUpdated,
version,
}) => {
return (
<div className="w-full max-w-[396px] px-4 sm:px-6 lg:w-[396px] lg:px-0">
{/* Title */}
<div className="font-poppins mb-3 w-full 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>
{/* Creator */}
<div className="mb-3 flex w-full items-center gap-1.5 lg:mb-4">
<div className="font-geist text-base font-normal text-neutral-800 dark:text-neutral-200 sm:text-lg lg:text-xl">
by
</div>
<div className="font-geist text-base font-medium text-neutral-800 dark:text-neutral-200 sm:text-lg lg:text-xl">
{creator}
</div>
</div>
{/* Short Description */}
<div className="font-geist mb-4 line-clamp-2 w-full 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>
{/* Run Agent Button */}
<div className="mb-4 w-full lg:mb-6">
<button className="inline-flex w-full items-center justify-center gap-2 rounded-[38px] bg-violet-600 px-4 py-3 transition-colors hover:bg-violet-700 sm:w-auto sm:gap-2.5 sm:px-5 sm:py-3.5 lg:px-6 lg:py-4">
<IconPlay className="h-5 w-5 text-white sm:h-5 sm:w-5 lg:h-6 lg:w-6" />
<span className="font-poppins text-base font-medium text-neutral-50 sm:text-lg">
Run agent
</span>
</button>
</div>
{/* Rating and Runs */}
<div className="mb-4 flex w-full items-center justify-between lg:mb-6">
<div className="flex items-center gap-1.5 sm:gap-2">
<span className="font-geist whitespace-nowrap 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="font-geist whitespace-nowrap text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
{runs.toLocaleString()} runs
</div>
</div>
{/* Separator */}
<Separator className="mb-4 lg:mb-6" />
{/* Description Section */}
<div className="mb-4 w-full lg:mb-6">
<div className="mb-1.5 text-xs font-medium text-neutral-800 dark:text-neutral-200 sm:mb-2 sm:text-sm">
Description
</div>
<div className="font-geist w-full whitespace-pre-line text-sm font-normal text-neutral-600 dark:text-neutral-300 sm:text-base">
{longDescription}
</div>
</div>
{/* Categories */}
<div className="mb-4 flex w-full flex-col gap-1.5 sm:gap-2 lg:mb-6">
<div className="text-xs font-medium text-neutral-800 dark:text-neutral-200 sm:text-sm">
Categories
</div>
<div className="flex flex-wrap gap-1.5 sm:gap-2">
{categories.map((category, index) => (
<div
key={index}
className="whitespace-nowrap rounded-full border border-neutral-200 bg-white px-2 py-0.5 text-xs text-neutral-800 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-200 sm:px-3 sm:py-1 sm:text-sm"
>
{category}
</div>
))}
</div>
</div>
{/* Version History */}
<div className="flex w-full flex-col gap-0.5 sm:gap-1">
<div className="text-xs font-medium text-neutral-800 dark:text-neutral-200 sm:text-sm">
Version history
</div>
<div className="text-xs text-neutral-600 dark:text-neutral-400 sm:text-sm">
Last updated {lastUpdated}
</div>
<div className="text-xs text-neutral-600 dark:text-neutral-400 sm:text-sm">
Version {version}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,85 @@
import type { Meta, StoryObj } from "@storybook/react";
import { AgentTable } from "./AgentTable";
import { AgentTableRowProps } from "./AgentTableRow";
import { userEvent, within, expect } from "@storybook/test";
import { StatusType } from "./Status";
const meta: Meta<typeof AgentTable> = {
title: "AGPT UI/Agent Table",
component: AgentTable,
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj<typeof AgentTable>;
const sampleAgents: AgentTableRowProps[] = [
{
id: "agent-1",
agentName: "Super Coder",
description: "An AI agent that writes clean, efficient code",
imageSrc:
"https://ddz4ak4pa3d19.cloudfront.net/cache/53/b2/53b2bc7d7900f0e1e60bf64ebf38032d.jpg",
dateSubmitted: "2023-05-15",
status: "approved",
runs: 1500,
rating: 4.8,
onEdit: () => console.log("Edit Super Coder"),
},
{
id: "agent-2",
agentName: "Data Analyzer",
description: "Processes and analyzes large datasets with ease",
imageSrc:
"https://ddz4ak4pa3d19.cloudfront.net/cache/40/f7/40f7bc97c952f8df0f9c88d29defe8d4.jpg",
dateSubmitted: "2023-05-10",
status: "awaiting_review",
runs: 1200,
rating: 4.5,
onEdit: () => console.log("Edit Data Analyzer"),
},
{
id: "agent-3",
agentName: "UI Designer",
description: "Creates beautiful and intuitive user interfaces",
imageSrc:
"https://ddz4ak4pa3d19.cloudfront.net/cache/14/9e/149ebb9014aa8c0097e72ed89845af0e.jpg",
dateSubmitted: "2023-05-05",
status: "draft",
runs: 800,
rating: 4.2,
onEdit: () => console.log("Edit UI Designer"),
},
];
export const Default: Story = {
args: {
agents: sampleAgents,
},
};
export const EmptyTable: Story = {
args: {
agents: [],
},
};
// 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 EmptyTableTest: Story = {
...EmptyTable,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const emptyMessage = canvas.getByText("No agents found");
expect(emptyMessage).toBeTruthy();
},
};

View File

@@ -0,0 +1,107 @@
"use client";
import * as React from "react";
import { AgentTableRow, AgentTableRowProps } from "./AgentTableRow";
import { AgentTableCard } from "./AgentTableCard";
import { StoreSubmissionRequest } from "@/lib/autogpt-server-api/types";
export interface AgentTableProps {
agents: AgentTableRowProps[];
onEditSubmission: (submission: StoreSubmissionRequest) => void;
onDeleteSubmission: (submission_id: string) => void;
}
export const AgentTable: React.FC<AgentTableProps> = ({
agents,
onEditSubmission,
onDeleteSubmission,
}) => {
// Use state to track selected agents
const [selectedAgents, setSelectedAgents] = React.useState<Set<string>>(
new Set(),
);
// Handle select all checkbox
const handleSelectAll = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.checked) {
setSelectedAgents(new Set(agents.map((agent) => agent.id.toString())));
} else {
setSelectedAgents(new Set());
}
},
[agents],
);
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>
{/* Table body */}
{agents.length > 0 ? (
<div className="flex flex-col">
{agents.map((agent, index) => (
<div key={agent.id} className="md:block">
<AgentTableRow
{...agent}
onEditSubmission={onEditSubmission}
onDeleteSubmission={onDeleteSubmission}
/>
<div className="block md:hidden">
<AgentTableCard {...agent} />
</div>
</div>
))}
</div>
) : (
<div className="py-4 text-center font-['Geist'] text-base text-neutral-600 dark:text-neutral-400">
No agents available. Create your first agent to get started!
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,65 @@
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,
onEdit: () => console.log("Edit Super Coder"),
},
};
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

@@ -0,0 +1,95 @@
"use client";
import * as React from "react";
import Image from "next/image";
import { IconStarFilled, IconMore } from "@/components/ui/icons";
import { Status, StatusType } from "./Status";
export interface AgentTableCardProps {
agent_id: string;
agent_version: number;
agentName: string;
sub_heading: string;
description: string;
imageSrc: string[];
dateSubmitted: string;
status: StatusType;
runs: number;
rating: number;
id: number;
onEditSubmission: (submission: StoreSubmissionRequest) => void;
}
export const AgentTableCard: React.FC<AgentTableCardProps> = ({
agent_id,
agent_version,
agentName,
sub_heading,
description,
imageSrc,
dateSubmitted,
status,
runs,
rating,
id,
onEditSubmission,
}) => {
const onEdit = () => {
console.log("Edit agent", agentName);
onEditSubmission({
agent_id,
agent_version,
slug: "",
name: agentName,
sub_heading,
description,
image_urls: imageSrc,
categories: [],
});
};
return (
<div className="border-b border-neutral-300 p-4 dark:border-neutral-700">
<div className="flex gap-4">
<div className="relative h-[56px] w-[100px] overflow-hidden rounded-lg bg-[#d9d9d9] dark:bg-neutral-800">
<Image
src={imageSrc?.[0] ?? "/nada.png"}
alt={agentName}
fill
style={{ objectFit: "cover" }}
/>
</div>
<div className="flex-1">
<h3 className="text-[15px] font-medium text-neutral-800 dark:text-neutral-200">
{agentName}
</h3>
<p className="line-clamp-2 text-sm text-neutral-600 dark:text-neutral-400">
{description}
</p>
</div>
<button
onClick={onEdit}
className="h-fit 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>
</div>
<div className="mt-4 flex flex-wrap gap-4">
<Status status={status} />
<div className="text-sm text-neutral-600 dark:text-neutral-400">
{dateSubmitted}
</div>
<div className="text-sm text-neutral-600 dark:text-neutral-400">
{runs.toLocaleString()} runs
</div>
<div className="flex items-center 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>
</div>
</div>
);
};

View File

@@ -0,0 +1,170 @@
"use client";
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 ContextMenu from "@radix-ui/react-context-menu";
import { TrashIcon } from "@radix-ui/react-icons";
import { StoreSubmissionRequest } from "@/lib/autogpt-server-api/types";
export interface AgentTableRowProps {
agent_id: string;
agent_version: number;
agentName: string;
sub_heading: string;
description: string;
imageSrc: string[];
date_submitted: string;
status: StatusType;
runs: number;
rating: number;
dateSubmitted: string;
id: number;
onEditSubmission: (submission: StoreSubmissionRequest) => void;
onDeleteSubmission: (submission_id: string) => void;
}
export const AgentTableRow: React.FC<AgentTableRowProps> = ({
agent_id,
agent_version,
agentName,
sub_heading,
description,
imageSrc,
dateSubmitted,
status,
runs,
rating,
id,
onEditSubmission,
onDeleteSubmission,
}) => {
// Create a unique ID for the checkbox
const checkboxId = `agent-${id}-checkbox`;
const handleEdit = React.useCallback(() => {
onEditSubmission({
agent_id,
agent_version,
slug: "",
name: agentName,
sub_heading,
description,
image_urls: imageSrc,
categories: [],
} as StoreSubmissionRequest);
}, [
agent_id,
agent_version,
agentName,
sub_heading,
description,
imageSrc,
onEditSubmission,
]);
const handleDelete = React.useCallback(() => {
onDeleteSubmission(agent_id);
}, [agent_id, onDeleteSubmission]);
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"
/>
{/* Single label instead of multiple */}
<label htmlFor={checkboxId} className="sr-only">
Select {agentName}
</label>
</div>
</div>
<div className="grid w-full grid-cols-[minmax(400px,1fr),180px,140px,100px,100px,40px] items-center gap-4">
{/* Agent info column */}
<div className="flex items-center gap-4">
<div className="relative h-[70px] w-[125px] overflow-hidden rounded-[10px] bg-[#d9d9d9] dark:bg-neutral-700">
<Image
src={imageSrc?.[0] ?? "/nada.png"}
alt={agentName}
fill
style={{ objectFit: "cover" }}
/>
</div>
<div className="flex flex-col">
<h3 className="text-[15px] font-medium text-neutral-800 dark:text-neutral-200">
{agentName}
</h3>
<p className="line-clamp-2 text-sm text-neutral-600 dark:text-neutral-400">
{description}
</p>
</div>
</div>
{/* Date column */}
<div className="pl-14 text-sm text-neutral-600 dark:text-neutral-400">
{dateSubmitted}
</div>
{/* Status column */}
<div>
<Status status={status} />
</div>
{/* Runs column */}
<div className="text-right text-sm text-neutral-600 dark:text-neutral-400">
{runs?.toLocaleString() ?? "0"}
</div>
{/* Reviews column */}
<div className="text-right">
{rating ? (
<div className="flex items-center justify-end gap-1">
<span className="text-sm font-medium text-neutral-800 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
</span>
)}
</div>
{/* Actions - Three dots menu */}
<div className="flex justify-end">
<ContextMenu.Root>
<ContextMenu.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>
</ContextMenu.Trigger>
<ContextMenu.Content className="z-10 rounded-xl border bg-white p-1 shadow-md dark:bg-gray-800">
<ContextMenu.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>
</ContextMenu.Item>
<ContextMenu.Separator className="my-1 h-px bg-gray-300 dark:bg-gray-600" />
<ContextMenu.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>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,62 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BecomeACreator } from "./BecomeACreator";
import { userEvent, within } from "@storybook/test";
const meta = {
title: "AGPT UI/Become A Creator",
component: BecomeACreator,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {
title: { control: "text" },
heading: { control: "text" },
description: { control: "text" },
buttonText: { control: "text" },
onButtonClick: { action: "buttonClicked" },
},
} satisfies Meta<typeof BecomeACreator>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
title: "Want to contribute?",
heading: "We're always looking for more Creators!",
description: "Join our ever-growing community of hackers and tinkerers",
buttonText: "Become a Creator",
onButtonClick: () => console.log("Button clicked"),
},
};
export const CustomText: Story = {
args: {
title: "Become a Creator Today!",
heading: "Join Our Creator Community",
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 = {
args: {
...Default.args,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByText("Become a Creator");
await userEvent.click(button);
},
};

View File

@@ -0,0 +1,63 @@
"use client";
import * as React from "react";
import { PublishAgentPopout } from "./composite/PublishAgentPopout";
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,
}) => {
const handleButtonClick = () => {
onButtonClick?.();
console.log("Become A Creator clicked");
};
return (
<div className="relative mx-auto h-auto min-h-[300px] w-full max-w-[1360px] md:min-h-[400px] lg:h-[459px]">
{/* Top border */}
<div className="left-0 top-0 h-px w-full bg-gray-200 dark:bg-gray-700" />
{/* Title */}
<h2 className="mb-8 mt-6 text-2xl leading-7 text-neutral-800 dark:text-neutral-200">
{title}
</h2>
{/* Content Container */}
<div className="absolute left-1/2 top-1/2 w-full max-w-[900px] -translate-x-1/2 -translate-y-1/2 px-4 pt-16 text-center md:px-6 md:pt-10 lg:px-0">
<h2 className="font-poppins mb-6 text-3xl font-semibold leading-tight text-neutral-950 dark:text-neutral-50 md:mb-8 md:text-4xl md:leading-[1.2] lg:mb-12 lg:text-5xl lg:leading-[54px]">
Build AI agents and share
<br />
<span className="text-violet-600 dark:text-violet-400">
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>
<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="font-poppins whitespace-nowrap text-base font-medium leading-normal text-neutral-50 md:text-lg md:leading-relaxed lg:text-xl lg:leading-7">
{buttonText}
</span>
</button>
}
/>
</div>
</div>
);
};

View File

@@ -0,0 +1,79 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BreadCrumbs } from "./BreadCrumbs";
import { userEvent, within } from "@storybook/test";
const meta = {
title: "AGPT UI/BreadCrumbs",
component: BreadCrumbs,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {
items: { control: "object" },
},
} satisfies Meta<typeof BreadCrumbs>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
items: [
{ name: "Home", link: "/" },
{ name: "Agents", link: "/agents" },
{ name: "SEO Optimizer", link: "/agents/seo-optimizer" },
],
},
};
export const SingleItem: Story = {
args: {
items: [{ name: "Home", link: "/" }],
},
};
export const LongPath: Story = {
args: {
items: [
{ name: "Home", link: "/" },
{ name: "Categories", link: "/categories" },
{ name: "AI Tools", link: "/categories/ai-tools" },
{ name: "Data Analysis", link: "/categories/ai-tools/data-analysis" },
{
name: "Data Analyzer",
link: "/categories/ai-tools/data-analysis/data-analyzer",
},
],
},
};
export const WithInteraction: Story = {
args: {
items: [
{ name: "Home", link: "/" },
{ name: "Agents", link: "/agents" },
{ name: "Task Planner", link: "/agents/task-planner" },
],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const homeLink = canvas.getByText("Home");
await userEvent.hover(homeLink);
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

@@ -0,0 +1,43 @@
import * as React from "react";
import Link from "next/link";
import { IconLeftArrow, IconRightArrow } from "@/components/ui/icons";
interface BreadcrumbItem {
name: string;
link: string;
}
interface BreadCrumbsProps {
items: BreadcrumbItem[];
}
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 min-h-[4.375rem] flex-wrap items-center justify-start gap-4 rounded-[5rem] bg-white dark:bg-neutral-900">
{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="font-['SF Pro'] text-center text-2xl font-normal text-black dark:text-neutral-100">
/
</span>
)}
</React.Fragment>
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,220 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";
import { userEvent, within, expect } from "@storybook/test";
const meta = {
title: "AGPT UI/Button",
component: Button,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {
variant: {
control: "select",
options: [
"default",
"destructive",
"outline",
"secondary",
"ghost",
"link",
],
},
size: {
control: "select",
options: ["default", "sm", "lg", "primary", "icon"],
},
disabled: {
control: "boolean",
},
asChild: {
control: "boolean",
},
children: {
control: "text",
},
onClick: { action: "clicked" },
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: "Button",
},
};
export const Interactive: Story = {
args: {
children: "Interactive Button",
},
argTypes: {
onClick: { action: "clicked" },
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole("button", { name: /Interactive Button/i });
await userEvent.click(button);
await expect(button).toHaveFocus();
},
};
export const Variants: Story = {
render: (args) => (
<div className="flex flex-wrap gap-2">
<Button {...args} variant="default">
Default
</Button>
<Button {...args} variant="destructive">
Destructive
</Button>
<Button {...args} variant="outline">
Outline
</Button>
<Button {...args} variant="secondary">
Secondary
</Button>
<Button {...args} variant="ghost">
Ghost
</Button>
<Button {...args} variant="link">
Link
</Button>
</div>
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const buttons = canvas.getAllByRole("button");
await expect(buttons).toHaveLength(6);
for (const button of buttons) {
await userEvent.hover(button);
await expect(button).toHaveAttribute(
"class",
expect.stringContaining("hover:"),
);
}
},
};
export const Sizes: Story = {
render: (args) => (
<div className="flex flex-wrap items-center gap-2">
<Button {...args} size="sm">
Small
</Button>
<Button {...args} size="default">
Default
</Button>
<Button {...args} size="lg">
Large
</Button>
<Button {...args} size="primary">
Primary
</Button>
<Button {...args} size="icon">
🚀
</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 = {
args: {
children: "Disabled Button",
disabled: true,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole("button", { name: /Disabled Button/i });
await expect(button).toBeDisabled();
await expect(button).toHaveAttribute(
"class",
expect.stringContaining("disabled:opacity-50"),
);
await expect(button).not.toHaveFocus();
},
};
export const WithIcon: Story = {
args: {
children: (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4"
>
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
</svg>
Button with Icon
</>
),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole("button", { name: /Button with Icon/i });
const icon = button.querySelector("svg");
await expect(icon).toBeInTheDocument();
await expect(button).toHaveTextContent("Button with Icon");
},
};
export const LoadingState: Story = {
args: {
children: "Loading...",
disabled: true,
},
render: (args) => (
<Button {...args}>
<svg
className="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
{args.children}
</Button>
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole("button", { name: /Loading.../i });
await expect(button).toBeDisabled();
const spinner = button.querySelector("svg");
await expect(spinner).toHaveClass("animate-spin");
},
};

View File

@@ -0,0 +1,69 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-[80px] text-xl font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 dark:focus-visible:ring-neutral-300 font-neue leading-9 tracking-tight",
{
variants: {
variant: {
default:
"bg-white border border-black/50 text-[#272727] hover:bg-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700",
destructive:
"bg-red-600 text-neutral-50 border border-red-500/50 hover:bg-red-500/90 dark:bg-red-700 dark:text-neutral-50 dark:hover:bg-red-600",
outline:
"bg-white border border-black/50 text-[#272727] hover:bg-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700",
secondary:
"bg-neutral-100 text-[#272727] border border-neutral-200 hover:bg-neutral-100/80 dark:bg-neutral-700 dark:text-neutral-100 dark:border-neutral-600 dark:hover:bg-neutral-600",
ghost:
"hover:bg-neutral-100 text-[#272727] dark:text-neutral-100 dark:hover:bg-neutral-700",
link: "text-[#272727] underline-offset-4 hover:underline dark:text-neutral-100",
},
size: {
default:
"h-10 px-4 py-2 text-sm sm:h-12 sm:px-5 sm:py-2.5 sm:text-base md:h-14 md:px-6 md:py-3 md:text-lg lg:h-[4.375rem] lg:px-[1.625rem] lg:py-[0.4375rem] lg:text-xl",
sm: "h-8 px-3 py-1.5 text-xs sm:h-9 sm:px-3.5 sm:py-2 sm:text-sm md:h-10 md:px-4 md:py-2 md:text-base lg:h-[3.125rem] lg:px-[1.25rem] lg:py-[0.3125rem] lg:text-sm",
lg: "h-12 px-5 py-2.5 text-lg sm:h-14 sm:px-6 sm:py-3 sm:text-xl md:h-16 md:px-7 md:py-3.5 md:text-2xl lg:h-[5.625rem] lg:px-[2rem] lg:py-[0.5625rem] lg:text-2xl",
primary:
"h-10 w-28 sm:h-12 sm:w-32 md:h-[4.375rem] md:w-[11rem] lg:h-[3.125rem] lg:w-[7rem]",
icon: "h-10 w-10 sm:h-12 sm:w-12 md:h-14 md:w-14 lg:h-[4.375rem] lg:w-[4.375rem]",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
variant?:
| "default"
| "destructive"
| "outline"
| "secondary"
| "ghost"
| "link";
size?: "default" | "sm" | "lg" | "primary" | "icon";
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };

View File

@@ -0,0 +1,78 @@
import type { Meta, StoryObj } from "@storybook/react";
import { CreatorCard } from "./CreatorCard";
import { userEvent, within } from "@storybook/test";
const meta = {
title: "AGPT UI/Creator Card",
component: CreatorCard,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {
creatorName: { control: "text" },
creatorImage: { control: "text" },
bio: { control: "text" },
agentsUploaded: { control: "number" },
onClick: { action: "clicked" },
avatarSrc: { control: "text" },
},
} satisfies Meta<typeof CreatorCard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
creatorName: "John Doe",
creatorImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
bio: "AI enthusiast and developer with a passion for creating innovative agents.",
agentsUploaded: 15,
onClick: () => console.log("Default CreatorCard clicked"),
avatarSrc: "https://github.com/shadcn.png",
},
};
export const NewCreator: Story = {
args: {
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"),
avatarSrc: "https://example.com/avatar2.jpg",
},
};
export const ExperiencedCreator: Story = {
args: {
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"),
avatarSrc: "https://example.com/avatar3.jpg",
},
};
export const WithInteraction: Story = {
args: {
creatorName: "Sam Brown",
creatorImage:
"https://framerusercontent.com/images/KCIpxr9f97EGJgpaoqnjKsrOPwI.jpg",
bio: "Exploring the frontiers of AI and its applications in everyday life.",
agentsUploaded: 30,
onClick: () => console.log("WithInteraction CreatorCard clicked"),
avatarSrc: "https://example.com/avatar4.jpg",
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const creatorCard = canvas.getByText("Sam Brown");
await userEvent.hover(creatorCard);
await userEvent.click(creatorCard);
},
};

View File

@@ -0,0 +1,66 @@
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
];
interface CreatorCardProps {
creatorName: string;
creatorImage: string;
bio: string;
agentsUploaded: number;
onClick: () => void;
index: number;
}
export const CreatorCard: React.FC<CreatorCardProps> = ({
creatorName,
creatorImage,
bio,
agentsUploaded,
onClick,
index,
}) => {
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`}
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>
<div className="flex flex-col gap-2">
<h3 className="font-poppins text-2xl font-semibold leading-tight text-neutral-900 dark:text-neutral-100">
{creatorName}
</h3>
<p className="font-geist text-sm font-normal leading-normal text-neutral-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>
);
};

View File

@@ -0,0 +1,55 @@
import type { Meta, StoryObj } from "@storybook/react";
import { CreatorInfoCard } from "./CreatorInfoCard";
const meta = {
title: "AGPT UI/Creator Info Card",
component: CreatorInfoCard,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {
username: { control: "text" },
handle: { control: "text" },
avatarSrc: { control: "text" },
categories: { control: "object" },
averageRating: { control: "number", min: 0, max: 5, step: 0.1 },
totalRuns: { control: "number" },
},
} satisfies Meta<typeof CreatorInfoCard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
username: "SignificantGravitas",
handle: "oliviagrace1421",
avatarSrc: "https://github.com/shadcn.png",
categories: ["Entertainment", "Business"],
averageRating: 4.7,
totalRuns: 1500,
},
};
export const NewCreator: Story = {
args: {
username: "AI Enthusiast",
handle: "ai_newbie",
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,
},
};

View File

@@ -0,0 +1,105 @@
import * as React from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { StarRatingIcons } from "@/components/ui/icons";
interface CreatorInfoCardProps {
username: string;
handle: string;
avatarSrc: string;
categories: string[];
averageRating: number;
totalRuns: number;
}
export const CreatorInfoCard: React.FC<CreatorInfoCardProps> = ({
username,
handle,
avatarSrc,
categories,
averageRating,
totalRuns,
}) => {
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"
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]">
<AvatarImage src={avatarSrc} alt={`${username}'s avatar`} />
<AvatarFallback className="h-[100px] w-[100px] sm:h-[130px] sm:w-[130px]">
{username.charAt(0)}
</AvatarFallback>
</Avatar>
<div className="flex w-full flex-col items-start justify-start gap-1.5">
<div className="font-poppins w-full text-2xl font-medium leading-8 text-neutral-900 dark:text-neutral-100 sm:text-[35px] sm:leading-10">
{username}
</div>
<div className="w-full font-neue text-lg font-normal leading-6 text-neutral-800 dark:text-neutral-200 sm:text-xl sm:leading-7">
@{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
</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 rounded-[34px] border border-neutral-600 px-5 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>
))}
</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-neue text-lg font-semibold leading-7 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-neue text-lg font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
{new Intl.NumberFormat().format(totalRuns)} runs
</div>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,71 @@
import type { Meta, StoryObj } from "@storybook/react";
import { CreatorLinks } from "./CreatorLinks";
const meta = {
title: "AGPT UI/Creator Links",
component: CreatorLinks,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {
links: {
control: "object",
description: "Object containing various social and web links",
},
},
} satisfies Meta<typeof CreatorLinks>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
links: {
website: "https://example.com",
linkedin: "https://linkedin.com/in/johndoe",
github: "https://github.com/johndoe",
other: ["https://twitter.com/johndoe", "https://medium.com/@johndoe"],
},
},
};
export const WebsiteOnly: Story = {
args: {
links: {
website: "https://example.com",
},
},
};
export const SocialLinks: Story = {
args: {
links: {
linkedin: "https://linkedin.com/in/janedoe",
github: "https://github.com/janedoe",
other: ["https://twitter.com/janedoe"],
},
},
};
export const NoLinks: Story = {
args: {
links: {},
},
};
export const MultipleOtherLinks: Story = {
args: {
links: {
website: "https://example.com",
linkedin: "https://linkedin.com/in/creator",
github: "https://github.com/creator",
other: [
"https://twitter.com/creator",
"https://medium.com/@creator",
"https://youtube.com/@creator",
"https://tiktok.com/@creator",
],
},
},
};

View File

@@ -0,0 +1,43 @@
import * as React from "react";
import { getIconForSocial } from "@/components/ui/icons";
interface CreatorLinksProps {
links: string[];
}
export const CreatorLinks: React.FC<CreatorLinksProps> = ({ links }) => {
if (!links || links.length === 0) {
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">
Other links
</div>
<div className="flex w-full flex-wrap gap-3">
{links.map((link, index) => (
<React.Fragment key={index}>{renderLinkButton(link)}</React.Fragment>
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,43 @@
import type { Meta, StoryObj } from "@storybook/react";
import CreditsCard from "./CreditsCard";
import { userEvent, within } from "@storybook/test";
const meta: Meta<typeof CreditsCard> = {
title: "AGPT UI/Credits Card",
component: CreditsCard,
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj<typeof CreditsCard>;
export const Default: Story = {
args: {
credits: 0,
},
};
export const SmallNumber: Story = {
args: {
credits: 10,
},
};
export const LargeNumber: Story = {
args: {
credits: 1000000,
},
};
export const InteractionTest: Story = {
args: {
credits: 100,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const refreshButton = canvas.getByRole("button", {
name: /refresh credits/i,
});
await userEvent.click(refreshButton);
},
};

View File

@@ -0,0 +1,53 @@
"use client";
import { IconRefresh } from "@/components/ui/icons";
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
import { useState } from "react";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
interface CreditsCardProps {
credits: number;
}
const CreditsCard = ({ credits }: CreditsCardProps) => {
const [currentCredits, setCurrentCredits] = useState(credits);
const api = new AutoGPTServerAPI();
const onRefresh = async () => {
const { credits } = await api.getUserCredit("credits-card");
setCurrentCredits(credits);
};
return (
<div className="inline-flex h-[60px] items-center gap-2.5 rounded-2xl bg-neutral-200 p-4 dark:bg-neutral-800">
<div className="flex items-center gap-0.5">
<span className="p-ui-semibold text-base leading-7 text-neutral-900 dark:text-neutral-50">
{currentCredits.toLocaleString()}
</span>
<span className="p-ui pl-1 text-base leading-7 text-neutral-900 dark:text-neutral-50">
credits
</span>
</div>
<Tooltip key="RefreshCredits" delayDuration={500}>
<TooltipTrigger asChild>
<button
onClick={onRefresh}
className="h-6 w-6 transition-colors hover:text-neutral-700 dark:hover:text-neutral-300"
aria-label="Refresh credits"
>
<IconRefresh className="h-6 w-6" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Refresh credits</p>
</TooltipContent>
</Tooltip>
</div>
);
};
export default CreditsCard;

View File

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

Some files were not shown because too many files have changed in this diff Show More