Files
AutoGPT/autogpt_platform/frontend/CONTRIBUTING.md
Ubbe b7646f3e58 docs(frontend): contributing guidelines (#11210)
## Changes 🏗️

Document how to contribute on the Front-end so it is easier for
non-regular contributors.

## Checklist 📋

### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
- [x] Contribution guidelines make sense and look good considering the
AutoGPT stack

### For configuration changes:

None
2025-10-21 08:26:51 +00:00

766 lines
23 KiB
Markdown

<div align="center">
<h1>AutoGPT Frontend • Contributing ⌨️</h1>
<p>Next.js App Router • Client-first • Type-safe generated API hooks • Tailwind + shadcn/ui</p>
</div>
---
## ☕️ Summary
This document is your reference for contributing to the AutoGPT Frontend. It adapts legacy guidelines to our current stack and practices.
- Architecture and stack
- Component structure and design system
- Data fetching (generated API hooks)
- Feature flags
- Naming and code conventions
- Tooling, scripts, and testing
- PR process and checklist
This is a living document. Open a pull request any time to improve it.
---
## 🚀 Quick Start FAQ
New to the codebase? Here are shortcuts to common tasks:
### I need to make a new page
1. Create page in `src/app/(platform)/your-feature/page.tsx`
2. If it has logic, create `usePage.ts` hook next to it
3. Create sub-components in `components/` folder
4. Use generated API hooks for data fetching
5. If page needs auth, ensure it's in the `(platform)` route group
**Example structure:**
```
app/(platform)/dashboard/
page.tsx
useDashboardPage.ts
components/
StatsPanel/
StatsPanel.tsx
useStatsPanel.ts
```
See [Component structure](#-component-structure) and [Styling](#-styling) and [Data fetching patterns](#-data-fetching-patterns) sections.
### I need to update an existing component in a page
1. Find the page `src/app/(platform)/your-feature/page.tsx`
2. Check its `components/` folder
3. If needing to update its logic, check the `use[Component].ts` hook
4. If the update is related to rendering, check `[Component].tsx` file
See [Component structure](#-component-structure) and [Styling](#-styling) sections.
### I need to make a new API call and show it on the UI
1. Ensure the backend endpoint exists in the OpenAPI spec
2. Regenerate API client: `pnpm generate:api`
3. Import the generated hook by typing the operation name (auto-import)
4. Use the hook in your component/custom hook
5. Handle loading, error, and success states
**Example:**
```tsx
import { useGetV2ListLibraryAgents } from "@/app/api/__generated__/endpoints/library/library";
export function useAgentList() {
const { data, isLoading, isError, error } = useGetV2ListLibraryAgents();
return {
agents: data?.data || [],
isLoading,
isError,
error,
};
}
```
See [Data fetching patterns](#-data-fetching-patterns) for more examples.
### I need to create a new component in the Design System
1. Determine the atomic level: atom, molecule, or organism
2. Create folder: `src/components/[level]/ComponentName/`
3. Create `ComponentName.tsx` (render logic)
4. If logic exists, create `useComponentName.ts`
5. Create `ComponentName.stories.tsx` for Storybook
6. Use Tailwind + design tokens (avoid hardcoded values)
7. Only use Phosphor icons
8. Test in Storybook: `pnpm storybook`
9. Verify in Chromatic after PR
**Example structure:**
```
src/components/molecules/DataCard/
DataCard.tsx
DataCard.stories.tsx
useDataCard.ts
```
See [Component structure](#-component-structure) and [Styling](#-styling) sections.
---
## 📟 Contribution process
### 1) Branch off `dev`
- Branch from `dev` for features and fixes
- Keep PRs focused (aim for one ticket per PR)
- Use conventional commit messages with a scope (e.g., `feat(frontend): add X`)
### 2) Feature flags
If a feature will ship across multiple PRs, guard it with a flag so we can merge iteratively.
- Use [LaunchDarkly](https://www.launchdarkly.com) based flags (see Feature Flags below)
- Avoid long-lived feature branches
### 3) Open PR and get reviews ✅
Before requesting review:
- [x] Code follows architecture and conventions here
- [x] `pnpm format && pnpm lint && pnpm types` pass
- [x] Relevant tests pass locally: `pnpm test` (and/or Storybook tests)
- [x] If touching UI, validate against our design system and stories
### 4) Merge to `dev`
- Use squash merges
- Follow conventional commit message format for the squash title
---
## 📂 Architecture & Stack
### Next.js App Router
- We use the [Next.js App Router](https://nextjs.org/docs/app) in `src/app`
- Use [route segments](https://nextjs.org/docs/app/building-your-application/routing) with semantic URLs; no `pages/`
### Component good practices
- Default to client components
- Use server components only when:
- SEO requires server-rendered HTML, or
- Extreme first-byte performance justifies it
- If you render server-side data, prefer server-side prefetch + client hydration (see examples below and [React Query SSR & Hydration](https://tanstack.com/query/latest/docs/framework/react/guides/ssr))
- Prefer using [Next.js API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) when possible over [server actions](https://nextjs.org/docs/14/app/building-your-application/data-fetching/server-actions-and-mutations)
- Keep components small and simple
- favour composition and splitting large components into smaller bits of UI
- [colocate state](https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster) when possible
- keep render/side-effects split for [separation of concerns](https://en.wikipedia.org/wiki/Separation_of_concerns)
- do not over-complicate or re-invent the wheel
**❓ Why a client-side first design vs server components/actions?**
While server components and actions are cool and cutting-edge, they introduce a layer of complexity which not always justified by the benefits they deliver. Defaulting to client-first keeps things simple in the mental model of the developer, specially for those developers less familiar with Next.js or heavy Front-end development.
### Data fetching: prefer generated API hooks
- We generate a type-safe client and React Query hooks from the backend OpenAPI spec via [Orval](https://orval.dev/)
- Prefer the generated hooks under `src/app/api/__generated__/endpoints/...`
- Treat `BackendAPI` and code under `src/lib/autogpt-server-api/*` as deprecated; do not introduce new usages
- Use [Zod](https://zod.dev/) schemas from the generated client where applicable
### State management
- Prefer [React Query](https://tanstack.com/query/latest/docs/framework/react/overview) for server state, colocated near consumers (see [state colocation](https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster))
- Co-locate UI state inside components/hooks; keep global state minimal
### Styling and components
- [Tailwind CSS](https://tailwindcss.com/docs) + [shadcn/ui](https://ui.shadcn.com/) ([Radix Primitives](https://www.radix-ui.com/docs/primitives/overview/introduction) under the hood)
- Use the design system under `src/components` for primitives and building blocks
- Do not use anything under `src/components/_legacy__`; migrate away from it when touching old code
- Reference the design system catalog on Chromatic: [`https://dev--670f94474adee5e32c896b98.chromatic.com/`](https://dev--670f94474adee5e32c896b98.chromatic.com/)
- Use the [`tailwind-scrollbar`](https://www.npmjs.com/package/tailwind-scrollbar) plugin utilities for scrollbar styling
---
## 🧱 Component structure
For components, separate render logic from data/behavior, and keep implementation details local.
**Most components should follow this structure.** Pages are just bigger components made of smaller ones, and sub-components can have their own nested sub-components when dealing with complex features.
### Basic structure
When a component has non-trivial logic:
```
FeatureX/
FeatureX.tsx (render logic only)
useFeatureX.ts (hook; data fetching, behavior, state)
helpers.ts (pure helpers used by the hook)
components/ (optional, subcomponents local to FeatureX)
```
### Example: Page with nested components
```tsx
// Page composition
app/(platform)/dashboard/
page.tsx
useDashboardPage.ts
components/ # (Sub-components the dashboard page is made of)
StatsPanel/
StatsPanel.tsx
useStatsPanel.ts
helpers.ts
components/ # (Sub-components belonging to StatsPanel)
StatCard/
StatCard.tsx
ActivityFeed/
ActivityFeed.tsx
useActivityFeed.ts
```
### Guidelines
- Prefer function declarations for components and handlers
- Only use arrow functions for small inline lambdas (e.g., in `map`)
- Avoid barrel files and `index.ts` re-exports
- Keep component files focused and readable; push complex logic to `helpers.ts`
- Abstract reusable, cross-feature logic into `src/services/` or `src/lib/utils.ts` as appropriate
- Build components encapsulated so they can be easily reused and abstracted elsewhere
- Nest sub-components within a `components/` folder when they're local to the parent feature
### Exceptions
When to simplify the structure:
**Small hook logic (3-4 lines)**
If the hook logic is minimal, keep it inline with the render function:
```tsx
export function ActivityAlert() {
const [isVisible, setIsVisible] = useState(true);
if (!isVisible) return null;
return (
<Alert onClose={() => setIsVisible(false)}>New activity detected</Alert>
);
}
```
**Render-only components**
Components with no hook logic can be direct files in `components/` without a folder:
```
components/
ActivityAlert.tsx (render-only, no folder needed)
StatsPanel/ (has hook logic, needs folder)
StatsPanel.tsx
useStatsPanel.ts
```
### Hook file structure
When separating logic into a custom hook:
```tsx
// useStatsPanel.ts
export function useStatsPanel() {
const [data, setData] = useState<Stats[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchStats().then(setData);
}, []);
return {
data,
isLoading,
refresh: () => fetchStats().then(setData),
};
}
```
Rules:
- **Always return an object** that exposes data and methods to the view
- **Export a single function** named after the component (e.g., `useStatsPanel` for `StatsPanel.tsx`)
- **Abstract into helpers.ts** when hook logic grows large, so the hook file remains readable by scanning without diving into implementation details
---
## 🔄 Data fetching patterns
All API hooks are generated from the backend OpenAPI specification using [Orval](https://orval.dev/). The hooks are type-safe and follow the operation names defined in the backend API.
### How to discover hooks
Most of the time you can rely on auto-import by typing the endpoint or operation name. Your IDE will suggest the generated hooks based on the OpenAPI operation IDs.
**Examples of hook naming patterns:**
- `GET /api/v1/notifications``useGetV1GetNotificationPreferences`
- `POST /api/v2/store/agents``usePostV2CreateStoreAgent`
- `DELETE /api/v2/store/submissions/{id}``useDeleteV2DeleteStoreSubmission`
- `GET /api/v2/library/agents``useGetV2ListLibraryAgents`
**Pattern**: `use{Method}{Version}{OperationName}`
You can also explore the generated hooks by browsing `src/app/api/__generated__/endpoints/` which is organized by API tags (e.g., `auth`, `store`, `library`).
**OpenAPI specs:**
- Production: [https://backend.agpt.co/openapi.json](https://backend.agpt.co/openapi.json)
- Staging: [https://dev-server.agpt.co/openapi.json](https://dev-server.agpt.co/openapi.json)
### Generated hooks (client)
Prefer the generated React Query hooks (via Orval + React Query):
```tsx
import { useGetV1GetNotificationPreferences } from "@/app/api/__generated__/endpoints/auth/auth";
export function PreferencesPanel() {
const { data, isLoading, isError } = useGetV1GetNotificationPreferences({
query: {
select: (res) => res.data,
},
});
if (isLoading) return null;
if (isError) throw new Error("Failed to load preferences");
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
```
### Generated mutations (client)
```tsx
import { useQueryClient } from "@tanstack/react-query";
import {
useDeleteV2DeleteStoreSubmission,
getGetV2ListMySubmissionsQueryKey,
} from "@/app/api/__generated__/endpoints/store/store";
export function DeleteSubmissionButton({
submissionId,
}: {
submissionId: string;
}) {
const queryClient = useQueryClient();
const { mutateAsync: deleteSubmission, isPending } =
useDeleteV2DeleteStoreSubmission({
mutation: {
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: getGetV2ListMySubmissionsQueryKey(),
});
},
},
});
async function onClick() {
await deleteSubmission({ submissionId });
}
return (
<button disabled={isPending} onClick={onClick}>
Delete
</button>
);
}
```
### Server-side prefetch + client hydration
Use server-side prefetch to improve TTFB while keeping the component tree client-first (see [React Query SSR & Hydration](https://tanstack.com/query/latest/docs/framework/react/guides/ssr)):
```tsx
// in a server component
import { getQueryClient } from "@/lib/tanstack-query/getQueryClient";
import { HydrationBoundary, dehydrate } from "@tanstack/react-query";
import {
prefetchGetV2ListStoreAgentsQuery,
prefetchGetV2ListStoreCreatorsQuery,
} from "@/app/api/__generated__/endpoints/store/store";
export default async function MarketplacePage() {
const queryClient = getQueryClient();
await Promise.all([
prefetchGetV2ListStoreAgentsQuery(queryClient, { featured: true }),
prefetchGetV2ListStoreAgentsQuery(queryClient, { sorted_by: "runs" }),
prefetchGetV2ListStoreCreatorsQuery(queryClient, {
featured: true,
sorted_by: "num_agents",
}),
]);
return (
<HydrationBoundary state={dehydrate(queryClient)}>
{/* Client component tree goes here */}
</HydrationBoundary>
);
}
```
Notes:
- Do not introduce new usages of `BackendAPI` or `src/lib/autogpt-server-api/*`
- Keep transformations and mapping logic close to the consumer (hook), not in the view
---
## ⚠️ Error handling
The app has multiple error handling strategies depending on the type of error:
### Render/runtime errors
Use `<ErrorCard />` to display render or runtime errors gracefully:
```tsx
import { ErrorCard } from "@/components/molecules/ErrorCard";
export function DataPanel() {
const { data, isLoading, isError, error } = useGetData();
if (isLoading) return <Skeleton />;
if (isError) return <ErrorCard error={error} />;
return <div>{data.content}</div>;
}
```
### API mutation errors
Display mutation errors using toast notifications:
```tsx
import { useToast } from "@/components/ui/use-toast";
export function useUpdateSettings() {
const { toast } = useToast();
const { mutateAsync: updateSettings } = useUpdateSettingsMutation({
mutation: {
onError: (error) => {
toast({
title: "Failed to update settings",
description: error.message,
variant: "destructive",
});
},
},
});
return { updateSettings };
}
```
### Manual Sentry capture
When needed, you can manually capture exceptions to Sentry:
```tsx
import * as Sentry from "@sentry/nextjs";
try {
await riskyOperation();
} catch (error) {
Sentry.captureException(error, {
tags: { context: "feature-x" },
extra: { metadata: additionalData },
});
throw error;
}
```
### Global error boundaries
The app has error boundaries already configured to:
- Capture uncaught errors globally and send them to Sentry
- Display a user-friendly error UI when something breaks
- Prevent the entire app from crashing
You don't need to wrap components in error boundaries manually unless you need custom error recovery logic.
---
## 🚩 Feature Flags
- Flags are powered by [LaunchDarkly](https://docs.launchdarkly.com/)
- Use the helper APIs under `src/services/feature-flags`
Check a flag in a client component:
```tsx
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
export function AgentActivityPanel() {
const enabled = useGetFlag(Flag.AGENT_ACTIVITY);
if (!enabled) return null;
return <div>Feature is enabled!</div>;
}
```
Protect a route or page component:
```tsx
import { withFeatureFlag } from "@/services/feature-flags/with-feature-flag";
export const MyFeaturePage = withFeatureFlag(function Page() {
return <div>My feature page</div>;
}, "my-feature-flag");
```
Local dev and Playwright:
- Set `NEXT_PUBLIC_PW_TEST=true` to use mocked flag values during local development and tests
Adding new flags:
1. Add the flag to the `Flag` enum and `FlagValues` type
2. Provide a mock value in the mock map
3. Configure the flag in LaunchDarkly
---
## 📙 Naming conventions
General:
- Variables and functions should read like plain English
- Prefer `const` over `let` unless reassignment is required
- Use searchable constants instead of magic numbers
Files:
- Components and hooks: `PascalCase` for component files, `camelCase` for hooks
- Other files: `kebab-case`
- Do not create barrel files or `index.ts` re-exports
Types:
- Prefer `interface` for object shapes
- Component props should be `interface Props { ... }`
- Use precise types; avoid `any` and unsafe casts
Parameters:
- If more than one parameter is needed, pass a single `Args` object for clarity
Comments:
- Keep comments minimal; code should be clear by itself
- Only document non-obvious intent, invariants, or caveats
Functions:
- Prefer function declarations for components and handlers
- Only use arrow functions for small inline callbacks
Control flow:
- Use early returns to reduce nesting
- Avoid catching errors unless you handle them meaningfully
---
## 🎨 Styling
- Use Tailwind utilities; prefer semantic, composable class names
- Use shadcn/ui components as building blocks when available
- Use the `tailwind-scrollbar` utilities for scrollbar styling
- Keep responsive and dark-mode behavior consistent with the design system
Additional requirements:
- Do not import shadcn primitives directly in feature code; only use components exposed in our design system under `src/components`. shadcn is a low-level skeleton we style on top of and is not meant to be consumed directly.
- Prefer design tokens over Tailwind's default theme whenever possible (e.g., color, spacing, radius, and typography tokens). Avoid hardcoded values and default palette if a token exists.
---
## ⚠️ Errors and ⏳ Loading
- **Errors**: Use the `ErrorCard` component from the design system to display API/HTTP errors and retry actions. Keep error derivation/mapping in hooks; pass the final message to the component.
- Component: `src/components/molecules/ErrorCard/ErrorCard.tsx`
- **Loading**: Use the `Skeleton` component(s) from the design system for loading states. Favor domain-appropriate skeleton layouts (lists, cards, tables) over spinners.
- See Storybook examples under Atoms/Skeleton for patterns.
---
## 🧭 Responsive and mobile-first
- Build mobile-first. Ensure new UI looks great from a 375px viewport width (iPhone SE) upwards.
- Validate layouts at common breakpoints (375, 768, 1024, 1280). Prefer stacking and progressive disclosure on small screens.
---
## 🧰 State for complex flows
For components/flows with complex state, multi-step wizards, or cross-component coordination, prefer a small co-located store using [Zustand](https://github.com/pmndrs/zustand).
Guidelines:
- Co-locate the store with the feature (e.g., `FeatureX/store.ts`).
- Expose typed selectors to minimize re-renders.
- Keep effects and API calls in hooks; stores hold state and pure actions.
Example: simple store with selectors
```ts
import { create } from "zustand";
interface WizardState {
step: number;
data: Record<string, unknown>;
next(): void;
back(): void;
setField(args: { key: string; value: unknown }): void;
}
export const useWizardStore = create<WizardState>((set) => ({
step: 0,
data: {},
next() {
set((state) => ({ step: state.step + 1 }));
},
back() {
set((state) => ({ step: Math.max(0, state.step - 1) }));
},
setField({ key, value }) {
set((state) => ({ data: { ...state.data, [key]: value } }));
},
}));
// Usage in a component (selectors keep updates scoped)
function WizardFooter() {
const step = useWizardStore((s) => s.step);
const next = useWizardStore((s) => s.next);
const back = useWizardStore((s) => s.back);
return (
<div className="flex items-center gap-2">
<button onClick={back} disabled={step === 0}>Back</button>
<button onClick={next}>Next</button>
</div>
);
}
```
Example: async action coordinated via hook + store
```ts
// FeatureX/useFeatureX.ts
import { useMutation } from "@tanstack/react-query";
import { useWizardStore } from "./store";
export function useFeatureX() {
const setField = useWizardStore((s) => s.setField);
const next = useWizardStore((s) => s.next);
const { mutateAsync: save, isPending } = useMutation({
mutationFn: async (payload: unknown) => {
// call API here
return payload;
},
onSuccess(data) {
setField({ key: "result", value: data });
next();
},
});
return { save, isSaving: isPending };
}
```
---
## 🖼 Icons
- Only use Phosphor Icons. Treat all other icon libraries as deprecated for new code.
- Package: `@phosphor-icons/react`
- Site: [`https://phosphoricons.com/`](https://phosphoricons.com/)
Example usage:
```tsx
import { Plus } from "@phosphor-icons/react";
export function CreateButton() {
return (
<button type="button" className="inline-flex items-center gap-2">
<Plus size={16} />
Create
</button>
);
}
```
---
## 🧪 Testing & Storybook
- End-to-end: [Playwright](https://playwright.dev/docs/intro) (`pnpm test`, `pnpm test-ui`)
- [Storybook](https://storybook.js.org/docs) for isolated UI development (`pnpm storybook` / `pnpm build-storybook`)
- For Storybook tests in CI, see [`@storybook/test-runner`](https://storybook.js.org/docs/writing-tests/test-runner) (`test-storybook:ci`)
- When changing components in `src/components`, update or add stories and visually verify in Storybook/Chromatic
---
## 🛠 Tooling & Scripts
Common scripts (see `package.json` for full list):
- `pnpm dev` — Start Next.js dev server (generates API client first)
- `pnpm build` — Build for production
- `pnpm start` — Start production server
- `pnpm lint` — ESLint + Prettier check
- `pnpm format` — Format code
- `pnpm types` — Type-check
- `pnpm storybook` — Run Storybook
- `pnpm test` — Run Playwright tests
Generated API client:
- `pnpm generate:api` — Fetch OpenAPI spec and regenerate the client
---
## ✅ PR checklist (Frontend)
- Client-first: server components only for SEO or extreme TTFB needs
- Uses generated API hooks; no new `BackendAPI` usages
- UI uses `src/components` primitives; no new `_legacy__` components
- Logic is separated into `use*.ts` and `helpers.ts` when non-trivial
- Reusable logic extracted to `src/services/` or `src/lib/utils.ts` when appropriate
- Navigation uses the Next.js router
- Lint, format, type-check, and tests pass locally
- Stories updated/added if UI changed; verified in Storybook
---
## ♻️ Migration guidance
When touching legacy code:
- Replace usages of `src/components/_legacy__/*` with the modern design system components under `src/components`
- Replace `BackendAPI` or `src/lib/autogpt-server-api/*` with generated API hooks
- Move presentational logic into render files and data/behavior into hooks
- Keep one-off transformations in local `helpers.ts`; move reusable logic to `src/services/` or `src/lib/utils.ts`
---
## 📚 References
- Design system (Chromatic): [`https://dev--670f94474adee5e32c896b98.chromatic.com/`](https://dev--670f94474adee5e32c896b98.chromatic.com/)
- Project README for setup and API client examples: `autogpt_platform/frontend/README.md`
- Conventional Commits: [conventionalcommits.org](https://www.conventionalcommits.org/)