diff --git a/.github/workflows/platform-frontend-ci.yml b/.github/workflows/platform-frontend-ci.yml index 4f729995a869..f4276784cbfa 100644 --- a/.github/workflows/platform-frontend-ci.yml +++ b/.github/workflows/platform-frontend-ci.yml @@ -56,6 +56,30 @@ jobs: run: | yarn type-check + design: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "21" + + - name: Install dependencies + run: | + yarn install --frozen-lockfile + + - name: Run Chromatic + uses: chromaui/action@latest + with: + # ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret + projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + workingDir: autogpt_platform/frontend + test: runs-on: ubuntu-latest strategy: diff --git a/autogpt_platform/frontend/.storybook/main.ts b/autogpt_platform/frontend/.storybook/main.ts index 62db72c32018..75b1365a01cf 100644 --- a/autogpt_platform/frontend/.storybook/main.ts +++ b/autogpt_platform/frontend/.storybook/main.ts @@ -9,6 +9,10 @@ const config: StorybookConfig = { "@storybook/addon-essentials", "@storybook/addon-interactions", ], + env: { + NEXT_PUBLIC_SUPABASE_URL: "https://your-project.supabase.co", + NEXT_PUBLIC_SUPABASE_ANON_KEY: "your-anon-key", + }, features: { experimentalRSC: true, }, @@ -16,6 +20,16 @@ const config: StorybookConfig = { name: "@storybook/nextjs", options: {}, }, - staticDirs: ["../public"], + staticDirs: [ + "../public", + { + from: "../node_modules/geist/dist/fonts/geist-sans", + to: "/fonts/geist-sans", + }, + { + from: "../node_modules/geist/dist/fonts/geist-mono", + to: "/fonts/geist-mono", + }, + ], }; export default config; diff --git a/autogpt_platform/frontend/.storybook/preview.ts b/autogpt_platform/frontend/.storybook/preview.ts deleted file mode 100644 index b8bef1a320f4..000000000000 --- a/autogpt_platform/frontend/.storybook/preview.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Preview } from "@storybook/react"; -import { initialize, mswLoader } from "msw-storybook-addon"; -import "../src/app/globals.css"; - -// Initialize MSW -initialize(); - -const preview: Preview = { - parameters: { - nextjs: { - appDirectory: true, - }, - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/i, - }, - }, - }, - loaders: [mswLoader], -}; - -export default preview; diff --git a/autogpt_platform/frontend/.storybook/preview.tsx b/autogpt_platform/frontend/.storybook/preview.tsx new file mode 100644 index 000000000000..8b3cab24c27d --- /dev/null +++ b/autogpt_platform/frontend/.storybook/preview.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import type { Preview } from "@storybook/react"; +import { initialize, mswLoader } from "msw-storybook-addon"; +import { Inter, Poppins } from "next/font/google"; +import localFont from "next/font/local"; +import "../src/app/globals.css"; +import { Providers } from "../src/app/providers"; + +const poppins = Poppins({ + subsets: ["latin"], + weight: ["400", "500", "600", "700"], + variable: "--font-poppins", +}); + +const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); + +const GeistSans = localFont({ + src: "../fonts/geist-sans/Geist-Variable.woff2", + variable: "--font-geist-sans", +}); + +const GeistMono = localFont({ + src: "../fonts/geist-mono/GeistMono-Variable.woff2", + variable: "--font-geist-mono", +}); + +// Initialize MSW +initialize(); + +const preview: Preview = { + parameters: { + nextjs: { + appDirectory: true, + }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + layout: "fullscreen", + }, + decorators: [ + (Story, context) => { + const mockOptions = context.parameters.mockBackend || {}; + + return ( +
+ + + +
+ ); + }, + ], + loaders: [mswLoader], +}; + +export default preview; diff --git a/autogpt_platform/frontend/.storybook/stoybook_guide.md b/autogpt_platform/frontend/.storybook/stoybook_guide.md new file mode 100644 index 000000000000..15fdcb7c68db --- /dev/null +++ b/autogpt_platform/frontend/.storybook/stoybook_guide.md @@ -0,0 +1,60 @@ +# For Client-Side Components + +When communicating with the server in client components, use the `useBackendAPI` hook. It automatically detects when running in a Storybook environment and switches to the mock client. + +To provide custom mock data instead of the default values, add the `mockBackend` parameter in your stories: + +```tsx +export const MyStory = { + parameters: { + mockBackend: { + credits: 100, + isAuthenticated: true, + // Other custom mock data + }, + }, +}; +``` + +# For Server-Side Components + +The server-based Supabase client automatically switches between real requests and mock responses. + +For server-side components, use the following pattern to select between backend client and mock client: + +```tsx +const api = process.env.STORYBOOK ? new MockClient() : new BackendAPI(); +``` + +You need to override specific API request methods in your mock client implementation. If you don't override a method, you can use the default methods provided by `BackendAPI` in both server-side and client-side environments. + +To use custom mock data in server components, pass it directly to the `MockClient` constructor: + +```tsx +const api = process.env.STORYBOOK + ? new MockClient({ credits: 200, isAuthenticated: true }) + : new BackendAPI(); +``` + +> Note: For client components, always use the `mockBackend` parameter in stories instead. + +# Using MSW + +If you haven't overridden an API request method in your mock client, you can use Mock Service Worker (MSW) to intercept HTTP requests from both the browser and Node.js environments, then respond with custom mock data: + +```tsx +// In your story +export const WithMSW = { + parameters: { + msw: { + handlers: [ + http.get("/api/user", () => { + return HttpResponse.json({ name: "John", role: "admin" }); + }), + ], + }, + }, +}; +``` + +Currently, it doesn't have support for client-side Supabase client and custom data for Supabase server-side client. You could use MSW for both cases. diff --git a/autogpt_platform/frontend/public/agpt-logo.png b/autogpt_platform/frontend/public/agpt-logo.png new file mode 100644 index 000000000000..ffb6947bd24c Binary files /dev/null and b/autogpt_platform/frontend/public/agpt-logo.png differ diff --git a/autogpt_platform/frontend/public/agpt-logo.svg b/autogpt_platform/frontend/public/agpt-logo.svg new file mode 100644 index 000000000000..1f3d1ac9ec29 --- /dev/null +++ b/autogpt_platform/frontend/public/agpt-logo.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/autogpt_platform/frontend/public/testing_agent_image.jpg b/autogpt_platform/frontend/public/testing_agent_image.jpg new file mode 100644 index 000000000000..c30b04fda3d9 Binary files /dev/null and b/autogpt_platform/frontend/public/testing_agent_image.jpg differ diff --git a/autogpt_platform/frontend/public/testing_avatar.png b/autogpt_platform/frontend/public/testing_avatar.png new file mode 100644 index 000000000000..19859c96d867 Binary files /dev/null and b/autogpt_platform/frontend/public/testing_avatar.png differ diff --git a/autogpt_platform/frontend/src/app/(platform)/layout.tsx b/autogpt_platform/frontend/src/app/(platform)/layout.tsx index 10a278f5f7ba..05ebe203c03e 100644 --- a/autogpt_platform/frontend/src/app/(platform)/layout.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/layout.tsx @@ -8,13 +8,14 @@ export default function PlatformLayout({ children }: { children: ReactNode }) { -
{children}
+
{children}
); } diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/agent/[creator]/[slug]/page.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/agent/[creator]/[slug]/page.tsx index c0e1115fdff7..05bee18907fe 100644 --- a/autogpt_platform/frontend/src/app/(platform)/marketplace/agent/[creator]/[slug]/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/agent/[creator]/[slug]/page.tsx @@ -56,62 +56,49 @@ export default async function Page({ const breadcrumbs = [ { name: "Marketplace", link: "/marketplace" }, - { - name: agent.creator, - link: `/marketplace/creator/${encodeURIComponent(agent.creator)}`, - }, { name: agent.agent_name, link: "#" }, ]; return ( -
-
- - -
-
- -
- + +
+
+
- - - - - - -
-
+ + + + + + + + ); } diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/creator/[creator]/page.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/creator/[creator]/page.tsx index fe70af696801..5fd5223ceb44 100644 --- a/autogpt_platform/frontend/src/app/(platform)/marketplace/creator/[creator]/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/creator/[creator]/page.tsx @@ -44,50 +44,47 @@ export default async function Page({ const creatorAgents = await api.getStoreAgents({ creator: params.creator }); return ( -
-
- +
+ -
-
- -
-
-

+

+
+ +
+
+
+

About

-
+

{creator.description} -

- - +
+ +
-
- - -
-
-
+ + + + + ); } catch (error) { return ( diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/page.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/page.tsx index 1336c71f0586..c265b1713cea 100644 --- a/autogpt_platform/frontend/src/app/(platform)/marketplace/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/page.tsx @@ -149,27 +149,28 @@ export default async function Page({}: {}) { const { featuredAgents, topAgents, featuredCreators } = await getStoreData(); return ( -
-
+
+
- - {/* 100px margin because our featured sections button are placed 40px below the container */} - +
+ +
+ {/* Below Separator's mt is 44px as per design; I need to add extra to counter the absolute positioning of the arrows above */} + - + - + -
-
+ + ); } diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/search/page.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/search/page.tsx index 717ac96cb549..837ccf5cfd0e 100644 --- a/autogpt_platform/frontend/src/app/(platform)/marketplace/search/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/search/page.tsx @@ -116,67 +116,67 @@ function SearchResults({ }; return ( -
-
-
-
-

- Results for: -

-

- {searchTerm} -

-
-
- -
+
+
+
+

+ Results for: +

+

+ {searchTerm} +

+
+ +
+
- {isLoading ? ( -
-

Loading...

+ {isLoading ? ( +
+

Loading...

+
+ ) : totalCount > 0 ? ( + <> +
+ +
- ) : totalCount > 0 ? ( - <> -
- - -
- {/* Content section */} -
- {showAgents && agentsCount > 0 && ( -
- -
- )} - - {showAgents && agentsCount > 0 && creatorsCount > 0 && ( - - )} - {showCreators && creatorsCount > 0 && ( + {/* Content section */} +
+ {showAgents && agentsCount > 0 && ( +
+ +
+ )} + + {showAgents && agentsCount > 0 && creatorsCount > 0 && ( + + )} + {showCreators && creatorsCount > 0 && ( +
- )} -
- - ) : ( -
-

- No results found -

-

- Try adjusting your search terms or filters -

+
+ )}
- )} -
+ + ) : ( +
+

+ No results found +

+

+ Try adjusting your search terms or filters +

+
+ )}
); } diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api_keys/page.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api_keys/page.tsx index 87f3d58b4eb3..14eee832e368 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api_keys/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api_keys/page.tsx @@ -2,9 +2,12 @@ import { APIKeysSection } from "@/components/agptui/composite/APIKeySection"; const ApiKeysPage = () => { return ( -
+
+

+ API key +

-
+ ); }; diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/credits/page.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/credits/page.tsx index 5ca249f48994..03684d7a5563 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/credits/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/credits/page.tsx @@ -104,8 +104,8 @@ export default function CreditsPage() { }; return ( -
-

+
+

Billing

diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/page.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/page.tsx index 60ab478e9082..35ab9746b54b 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/page.tsx @@ -13,6 +13,7 @@ import { } from "@/lib/autogpt-server-api/types"; import useSupabase from "@/hooks/useSupabase"; import { useBackendAPI } from "@/lib/autogpt-server-api/context"; +import AutogptButton from "@/components/agptui/AutogptButton"; export default function Page({}: {}) { const { supabase } = useSupabase(); @@ -64,70 +65,71 @@ export default function Page({}: {}) { }, []); return ( -
- {/* Header Section */} -
-
-

- Agent dashboard -

-
-

+
+ {/* Title */} +

+ Agent dashboard +

+ + {/* Content */} +
+
+
+

Submit a New Agent

-

+ +

Select from the list of agents you currently have, or upload from your local machine.

-
- - Submit agent - - } - openPopout={openPopout} - inputStep={popoutStep} - submissionData={submissionData} - /> -

- - - {/* Agents Section */} -
-

- Your uploaded agents -

- {submissions && ( - ({ - 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, - })) || [] + + Add to Library + } - onEditSubmission={onEditSubmission} - onDeleteSubmission={onDeleteSubmission} + openPopout={openPopout} + inputStep={popoutStep} + submissionData={submissionData} /> - )} -
+
+ + + +
+

+ Your uploaded agents +

+ + {submissions && ( + ({ + id: index, + agent_id: submission.agent_id, + agent_version: submission.agent_version, + sub_heading: submission.sub_heading, + date_submitted: submission.date_submitted, + agentName: submission.name, + description: submission.description, + imageSrc: submission.image_urls || [""], + dateSubmitted: new Date( + submission.date_submitted, + ).toLocaleDateString(), + status: submission.status.toLowerCase() as StatusType, + runs: submission.runs, + rating: submission.rating, + })) || [] + } + onEditSubmission={onEditSubmission} + onDeleteSubmission={onDeleteSubmission} + /> + )} +
+
); } diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/integrations/page.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/integrations/page.tsx index 91f01e8c991b..8acd04f12b9d 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/integrations/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/integrations/page.tsx @@ -149,83 +149,91 @@ export default function PrivatePage() { : []; return ( -
-

Connections & Credentials

- - - - Provider - Name - Actions - - - - {allCredentials.map((cred) => ( - - -
- - {cred.providerName} -
-
- -
- - {cred.title || cred.username} -
- - { - { - oauth2: "OAuth2 credentials", - api_key: "API key", - user_password: "Username & password", - }[cred.type] - }{" "} - - {cred.id} - -
- - - +
+

+ Profile +

+
+

+ Connections & Credentials +

+
+ + + Provider + Name + Actions - ))} - -
+ + + {allCredentials.map((cred) => ( + + +
+ + {cred.providerName} +
+
+ +
+ + {cred.title || cred.username} +
+ + { + { + oauth2: "OAuth2 credentials", + api_key: "API key", + user_password: "Username & password", + }[cred.type] + }{" "} + - {cred.id} + +
+ + + +
+ ))} +
+ - - - - Are you sure? - - {confirmationDialogState.open && confirmationDialogState.message} - - - - - confirmationDialogState.open && - confirmationDialogState.onReject() - } - > - Cancel - - - confirmationDialogState.open && - confirmationDialogState.onConfirm() - } - > - Continue - - - - + + + + Are you sure? + + {confirmationDialogState.open && + confirmationDialogState.message} + + + + + confirmationDialogState.open && + confirmationDialogState.onReject() + } + > + Cancel + + + confirmationDialogState.open && + confirmationDialogState.onConfirm() + } + > + Continue + + + + +
); } diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/layout.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/layout.tsx index 06b9cff300e1..b4db9745704b 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/layout.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/layout.tsx @@ -14,47 +14,49 @@ export default function Layout({ children }: { children: React.ReactNode }) { { links: [ { - text: "Creator Dashboard", - href: "/profile/dashboard", - icon: , + text: "API Keys", + href: "/profile/api_keys", + icon: , }, ...(process.env.NEXT_PUBLIC_SHOW_BILLING_PAGE === "true" ? [ { text: "Billing", href: "/profile/credits", - icon: , + icon: , }, ] : []), { - text: "Integrations", - href: "/profile/integrations", - icon: , + text: "Creator Dashboard", + href: "/profile/dashboard", + icon: , }, + { - text: "API Keys", - href: "/profile/api_keys", - icon: , + text: "Integrations", + href: "/profile/integrations", + icon: , }, + { text: "Profile", href: "/profile", - icon: , + icon: , }, { text: "Settings", href: "/profile/settings", - icon: , + icon: , }, ], }, ]; return ( -
+
-
{children}
+
{children}
); } diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/page.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/page.tsx index 14874dfa542e..702f12081ee2 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/page.tsx @@ -30,7 +30,10 @@ export default async function Page({}: {}) { } return ( -
+
+

+ Profile +

); diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/page.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/page.tsx index 428b98b077bf..314e22b5698b 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/page.tsx @@ -19,13 +19,12 @@ export default async function SettingsPage() { const preferences = await getUserPreferences(); return ( -
-
-

My account

-

- Manage your account settings and preferences. -

-
+
+ {/* Title */} +

+ Settings +

+
); diff --git a/autogpt_platform/frontend/src/app/layout.tsx b/autogpt_platform/frontend/src/app/layout.tsx index 999d23516fea..d2535ce2e5fa 100644 --- a/autogpt_platform/frontend/src/app/layout.tsx +++ b/autogpt_platform/frontend/src/app/layout.tsx @@ -38,7 +38,7 @@ export default async function RootLayout({ > diff --git a/autogpt_platform/frontend/src/app/providers.tsx b/autogpt_platform/frontend/src/app/providers.tsx index db9725d3f264..57eb1c4706ec 100644 --- a/autogpt_platform/frontend/src/app/providers.tsx +++ b/autogpt_platform/frontend/src/app/providers.tsx @@ -8,15 +8,30 @@ import { TooltipProvider } from "@/components/ui/tooltip"; import CredentialsProvider from "@/components/integrations/credentials-provider"; import { LaunchDarklyProvider } from "@/components/feature-flag/feature-flag-provider"; import OnboardingProvider from "@/components/onboarding/onboarding-provider"; +import { MockClientProps } from "@/lib/autogpt-server-api/mock_client"; +import PageStructureContainer from "@/components/page-structure-container-provider"; -export function Providers({ children, ...props }: ThemeProviderProps) { +export interface ProvidersProps extends ThemeProviderProps { + children: React.ReactNode; + useMockBackend?: boolean; + mockClientProps?: MockClientProps; +} + +export function Providers({ + children, + useMockBackend, + mockClientProps, + ...props +}: ProvidersProps) { return ( - + - {children} + + {children} + diff --git a/autogpt_platform/frontend/src/components/agptui/AgentImageItem.tsx b/autogpt_platform/frontend/src/components/agptui/AgentImageItem.tsx index ff028e313ada..991da070b474 100644 --- a/autogpt_platform/frontend/src/components/agptui/AgentImageItem.tsx +++ b/autogpt_platform/frontend/src/components/agptui/AgentImageItem.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import Image from "next/image"; import { PlayIcon } from "@radix-ui/react-icons"; import { Button } from "./Button"; +import AutogptButton from "./AutogptButton"; const isValidVideoFile = (url: string): boolean => { const videoExtensions = /\.(mp4|webm|ogg)$/i; @@ -32,6 +33,8 @@ interface AgentImageItemProps { export const AgentImageItem: React.FC = React.memo( ({ image, index, playingVideoIndex, handlePlay, handlePause }) => { const videoRef = React.useRef(null); + const [isVideoPlaying, setIsVideoPlaying] = React.useState(false); + const [thumbnail, setThumbnail] = React.useState(""); React.useEffect(() => { if ( @@ -43,11 +46,22 @@ export const AgentImageItem: React.FC = React.memo( } }, [playingVideoIndex, index]); + React.useEffect(() => { + if (videoRef.current && isValidVideoFile(image)) { + videoRef.current.currentTime = 0.1; + const canvas = document.createElement("canvas"); + canvas.width = videoRef.current.videoWidth; + canvas.height = videoRef.current.videoHeight; + canvas.getContext("2d")?.drawImage(videoRef.current, 0, 0); + setThumbnail(canvas.toDataURL()); + } + }, [image]); + const isVideoFile = isValidVideoFile(image); return (
-
+
{isValidVideoUrl(image) ? ( getYouTubeVideoId(image) ? (