mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-22 13:38:10 -05:00
Compare commits
25 Commits
testing-cl
...
cursor/upd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1273f36a30 | ||
|
|
2212edbd15 | ||
|
|
1b9fa753db | ||
|
|
9cbed88dba | ||
|
|
707f2a7d5d | ||
|
|
65482a4912 | ||
|
|
dc3c5435fa | ||
|
|
46b7b7f248 | ||
|
|
6ca5b991df | ||
|
|
2d26229229 | ||
|
|
42f51379a8 | ||
|
|
255ee5d684 | ||
|
|
a81042154b | ||
|
|
18ab6fd298 | ||
|
|
8a8d897e3b | ||
|
|
c8cb6771a4 | ||
|
|
00a6e326e5 | ||
|
|
689c985dca | ||
|
|
c18099b72b | ||
|
|
481c5b157d | ||
|
|
b780e40db6 | ||
|
|
33d23a34bd | ||
|
|
fc9165e86b | ||
|
|
fe375ce1bd | ||
|
|
6ad6ed9f56 |
@@ -75,6 +75,7 @@
|
||||
"moment": "2.30.1",
|
||||
"next": "15.3.5",
|
||||
"next-themes": "0.4.6",
|
||||
"nuqs": "2.4.3",
|
||||
"party-js": "2.2.0",
|
||||
"react": "18.3.1",
|
||||
"react-day-picker": "9.8.0",
|
||||
|
||||
33
autogpt_platform/frontend/pnpm-lock.yaml
generated
33
autogpt_platform/frontend/pnpm-lock.yaml
generated
@@ -155,6 +155,9 @@ importers:
|
||||
next-themes:
|
||||
specifier: 0.4.6
|
||||
version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
nuqs:
|
||||
specifier: 2.4.3
|
||||
version: 2.4.3(next@15.3.3(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
party-js:
|
||||
specifier: 2.2.0
|
||||
version: 2.2.0
|
||||
@@ -5326,6 +5329,9 @@ packages:
|
||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
mitt@3.0.1:
|
||||
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||
|
||||
module-details-from-path@1.0.4:
|
||||
resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==}
|
||||
|
||||
@@ -5456,6 +5462,24 @@ packages:
|
||||
nth-check@2.1.1:
|
||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||
|
||||
nuqs@2.4.3:
|
||||
resolution: {integrity: sha512-BgtlYpvRwLYiJuWzxt34q2bXu/AIS66sLU1QePIMr2LWkb+XH0vKXdbLSgn9t6p7QKzwI7f38rX3Wl9llTXQ8Q==}
|
||||
peerDependencies:
|
||||
'@remix-run/react': '>=2'
|
||||
next: '>=14.2.0'
|
||||
react: '>=18.2.0 || ^19.0.0-0'
|
||||
react-router: ^6 || ^7
|
||||
react-router-dom: ^6 || ^7
|
||||
peerDependenciesMeta:
|
||||
'@remix-run/react':
|
||||
optional: true
|
||||
next:
|
||||
optional: true
|
||||
react-router:
|
||||
optional: true
|
||||
react-router-dom:
|
||||
optional: true
|
||||
|
||||
oas-kit-common@1.0.8:
|
||||
resolution: {integrity: sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==}
|
||||
|
||||
@@ -12989,6 +13013,8 @@ snapshots:
|
||||
|
||||
minipass@7.1.2: {}
|
||||
|
||||
mitt@3.0.1: {}
|
||||
|
||||
module-details-from-path@1.0.4: {}
|
||||
|
||||
moment@2.30.1: {}
|
||||
@@ -13151,6 +13177,13 @@ snapshots:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
||||
nuqs@2.4.3(next@15.3.3(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
mitt: 3.0.1
|
||||
react: 18.3.1
|
||||
optionalDependencies:
|
||||
next: 15.3.3(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
|
||||
oas-kit-common@1.0.8:
|
||||
dependencies:
|
||||
fast-safe-stringify: 2.1.1
|
||||
|
||||
6
autogpt_platform/frontend/public/google-logo.svg
Normal file
6
autogpt_platform/frontend/public/google-logo.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.99 10.1871C19.99 9.36767 19.9246 8.76973 19.7831 8.14966H10.1943V11.8493H15.8207C15.7062 12.7676 15.0943 14.1618 13.7567 15.0492L13.7398 15.1632L16.7444 17.4429L16.9637 17.4648C18.8825 15.7291 19.99 13.2042 19.99 10.1871Z" fill="#4285F4"/>
|
||||
<path d="M10.1943 19.9313C12.9592 19.9313 15.2429 19.0454 16.9637 17.4648L13.7567 15.0492C12.8697 15.6438 11.7348 16.0244 10.1943 16.0244C7.50242 16.0244 5.25023 14.2886 4.39644 11.9036L4.28823 11.9125L1.17021 14.2775L1.13477 14.3808C2.84508 17.8028 6.1992 19.9313 10.1943 19.9313Z" fill="#34A853"/>
|
||||
<path d="M4.39644 11.9036C4.1758 11.2746 4.04876 10.6013 4.04876 9.90569C4.04876 9.21011 4.1758 8.53684 4.38177 7.90781L4.37563 7.7883L1.20776 5.3801L1.13477 5.41253C0.436264 6.80439 0.0390625 8.35202 0.0390625 9.90569C0.0390625 11.4594 0.436264 13.007 1.13477 14.3808L4.39644 11.9036Z" fill="#FBBC05"/>
|
||||
<path d="M10.1943 3.78682C12.1168 3.78682 13.397 4.66154 14.1236 5.33481L17.0194 2.59768C15.2373 0.953818 12.9592 0 10.1943 0C6.1992 0 2.84508 2.12847 1.13477 5.41253L4.38177 7.90781C5.25023 5.52278 7.50242 3.78682 10.1943 3.78682Z" fill="#EB4335"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,12 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { GraphID } from "@/lib/autogpt-server-api/types";
|
||||
import FlowEditor from "@/components/Flow";
|
||||
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
|
||||
import { useEffect } from "react";
|
||||
import LoadingBox from "@/components/ui/loading";
|
||||
import { GraphID } from "@/lib/autogpt-server-api/types";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect } from "react";
|
||||
|
||||
export default function BuilderPage() {
|
||||
function BuilderContent() {
|
||||
const query = useSearchParams();
|
||||
const { completeStep } = useOnboarding();
|
||||
|
||||
@@ -15,12 +16,20 @@ export default function BuilderPage() {
|
||||
}, [completeStep]);
|
||||
|
||||
const _graphVersion = query.get("flowVersion");
|
||||
const graphVersion = _graphVersion ? parseInt(_graphVersion) : undefined
|
||||
const graphVersion = _graphVersion ? parseInt(_graphVersion) : undefined;
|
||||
return (
|
||||
<FlowEditor
|
||||
className="flow-container"
|
||||
flowID={query.get("flowID") as GraphID | null ?? undefined}
|
||||
flowID={(query.get("flowID") as GraphID | null) ?? undefined}
|
||||
flowVersion={graphVersion}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BuilderPage() {
|
||||
return (
|
||||
<Suspense fallback={<LoadingBox className="h-[80vh]" />}>
|
||||
<BuilderContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,67 +1,10 @@
|
||||
import { Navbar } from "@/components/layout/Navbar/Navbar";
|
||||
import { ReactNode } from "react";
|
||||
import { Navbar } from "@/components/agptui/Navbar";
|
||||
import { IconType } from "@/components/ui/icons";
|
||||
|
||||
export default function PlatformLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<Navbar
|
||||
links={[
|
||||
{
|
||||
name: "Marketplace",
|
||||
href: "/marketplace",
|
||||
},
|
||||
{
|
||||
name: "Library",
|
||||
href: "/library",
|
||||
},
|
||||
{
|
||||
name: "Build",
|
||||
href: "/build",
|
||||
},
|
||||
]}
|
||||
menuItemGroups={[
|
||||
{
|
||||
items: [
|
||||
{
|
||||
icon: IconType.Edit,
|
||||
text: "Edit profile",
|
||||
href: "/profile",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{
|
||||
icon: IconType.LayoutDashboard,
|
||||
text: "Creator Dashboard",
|
||||
href: "/profile/dashboard",
|
||||
},
|
||||
{
|
||||
icon: IconType.UploadCloud,
|
||||
text: "Publish an agent",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{
|
||||
icon: IconType.Settings,
|
||||
text: "Settings",
|
||||
href: "/profile/settings",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{
|
||||
icon: IconType.LogOut,
|
||||
text: "Log out",
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Navbar />
|
||||
<main>{children}</main>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useQueryState } from "nuqs";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -45,6 +46,7 @@ import { useToast } from "@/components/ui/use-toast";
|
||||
|
||||
export default function AgentRunsPage(): React.ReactElement {
|
||||
const { id: agentID }: { id: LibraryAgentID } = useParams();
|
||||
const [executionId, setExecutionId] = useQueryState("executionId");
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const api = useBackendAPI();
|
||||
@@ -202,6 +204,15 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
selectPreset,
|
||||
]);
|
||||
|
||||
// Check for execution ID in URL search params and select that run
|
||||
useEffect(() => {
|
||||
if (executionId) {
|
||||
selectRun(executionId as GraphExecutionID);
|
||||
// Clean up the URL parameter after selecting the run
|
||||
setExecutionId(null);
|
||||
}
|
||||
}, [executionId, selectRun, setExecutionId]);
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
refreshPageData();
|
||||
@@ -468,7 +479,7 @@ export default function AgentRunsPage(): React.ReactElement {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container justify-stretch p-0 lg:flex">
|
||||
<div className="container justify-stretch p-0 pt-16 lg:flex">
|
||||
{/* Sidebar w/ list of runs */}
|
||||
{/* TODO: render this below header in sm and md layouts */}
|
||||
<AgentRunsSelectorList
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import {
|
||||
ArrowBottomRightIcon,
|
||||
QuestionMarkCircledIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
|
||||
import { LibraryPageStateProvider } from "./components/state-provider";
|
||||
import LibraryActionHeader from "./components/LibraryActionHeader/LibraryActionHeader";
|
||||
import LibraryAgentList from "./components/LibraryAgentList/LibraryAgentList";
|
||||
import { LibraryPageStateProvider } from "./components/state-provider";
|
||||
|
||||
/**
|
||||
* LibraryPage Component
|
||||
@@ -17,7 +17,7 @@ import LibraryAgentList from "./components/LibraryAgentList/LibraryAgentList";
|
||||
*/
|
||||
export default function LibraryPage() {
|
||||
return (
|
||||
<main className="container min-h-screen space-y-4 pb-20 sm:px-8 md:px-12">
|
||||
<main className="pt-160sm:px-8 container min-h-screen space-y-4 pb-20 pt-16 md:px-12">
|
||||
<LibraryPageStateProvider>
|
||||
<LibraryActionHeader />
|
||||
<LibraryAgentList />
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use server";
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
|
||||
import { verifyTurnstileToken } from "@/lib/turnstile";
|
||||
import { loginFormSchema, LoginProvider } from "@/types/auth";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
import { loginFormSchema, LoginProvider } from "@/types/auth";
|
||||
import { verifyTurnstileToken } from "@/lib/turnstile";
|
||||
|
||||
async function shouldShowOnboarding() {
|
||||
const api = new BackendAPI();
|
||||
@@ -38,7 +38,6 @@ export async function login(
|
||||
const { error } = await supabase.auth.signInWithPassword(values);
|
||||
|
||||
if (error) {
|
||||
console.error("Error logging in:", error);
|
||||
return error.message;
|
||||
}
|
||||
|
||||
@@ -76,6 +75,11 @@ export async function providerLogin(provider: LoginProvider) {
|
||||
});
|
||||
|
||||
if (error) {
|
||||
// FIXME: supabase doesn't return the correct error message for this case
|
||||
if (error.message.includes("P0001")) {
|
||||
return "not_allowed";
|
||||
}
|
||||
|
||||
console.error("Error logging in", error);
|
||||
return error.message;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { AuthCard } from "@/components/auth/AuthCard";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export function LoadingLogin() {
|
||||
return (
|
||||
<div className="flex h-full min-h-[85vh] flex-col items-center justify-center">
|
||||
<AuthCard title="">
|
||||
<div className="w-full space-y-6">
|
||||
<Skeleton className="mx-auto h-8 w-48" />
|
||||
<Skeleton className="h-12 w-full rounded-md" />
|
||||
<div className="flex w-full items-center">
|
||||
<Skeleton className="h-px flex-1" />
|
||||
<Skeleton className="mx-3 h-4 w-6" />
|
||||
<Skeleton className="h-px flex-1" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-12" />
|
||||
<Skeleton className="h-12 w-full rounded-md" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-12 w-full rounded-md" />
|
||||
</div>
|
||||
<Skeleton className="h-16 w-full rounded-md" />
|
||||
<Skeleton className="h-12 w-full rounded-md" />
|
||||
<div className="flex justify-center space-x-1">
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<Skeleton className="h-4 w-12" />
|
||||
</div>
|
||||
</div>
|
||||
</AuthCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +1,16 @@
|
||||
"use client";
|
||||
import {
|
||||
AuthBottomText,
|
||||
AuthButton,
|
||||
AuthCard,
|
||||
AuthFeedback,
|
||||
AuthHeader,
|
||||
GoogleOAuthButton,
|
||||
PasswordInput,
|
||||
Turnstile,
|
||||
} from "@/components/auth";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import LoadingBox from "@/components/ui/loading";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Link } from "@/components/atoms/Link/Link";
|
||||
import { AuthCard } from "@/components/auth/AuthCard";
|
||||
import AuthFeedback from "@/components/auth/AuthFeedback";
|
||||
import { EmailNotAllowedModal } from "@/components/auth/EmailNotAllowedModal";
|
||||
import { GoogleLoadingModal } from "@/components/auth/GoogleLoadingModal";
|
||||
import { GoogleOAuthButton } from "@/components/auth/GoogleOAuthButton";
|
||||
import Turnstile from "@/components/auth/Turnstile";
|
||||
import { Form, FormField } from "@/components/ui/form";
|
||||
import { getBehaveAs } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import { LoadingLogin } from "./components/LoadingLogin";
|
||||
import { useLoginPage } from "./useLoginPage";
|
||||
|
||||
export default function LoginPage() {
|
||||
@@ -30,17 +20,18 @@ export default function LoginPage() {
|
||||
turnstile,
|
||||
captchaKey,
|
||||
isLoading,
|
||||
isCloudEnv,
|
||||
isLoggedIn,
|
||||
isUserLoading,
|
||||
isGoogleLoading,
|
||||
showNotAllowedModal,
|
||||
isSupabaseAvailable,
|
||||
handleSubmit,
|
||||
handleProviderLogin,
|
||||
handleCloseNotAllowedModal,
|
||||
} = useLoginPage();
|
||||
|
||||
if (isUserLoading || isLoggedIn) {
|
||||
return <LoadingBox className="h-[80vh]" />;
|
||||
return <LoadingLogin />;
|
||||
}
|
||||
|
||||
if (!isSupabaseAvailable) {
|
||||
@@ -52,99 +43,99 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthCard className="mx-auto">
|
||||
<AuthHeader>Login to your account</AuthHeader>
|
||||
|
||||
{isCloudEnv ? (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<GoogleOAuthButton
|
||||
onClick={() => handleProviderLogin("google")}
|
||||
isLoading={isGoogleLoading}
|
||||
disabled={isLoading}
|
||||
<div className="flex h-full min-h-[85vh] flex-col items-center justify-center">
|
||||
<AuthCard title="Login to your account">
|
||||
{true ? (
|
||||
<>
|
||||
<div className="mb-3 w-full">
|
||||
<GoogleOAuthButton
|
||||
onClick={() => handleProviderLogin("google")}
|
||||
isLoading={isGoogleLoading}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 flex w-full items-center">
|
||||
<div className="h-px flex-1 bg-gray-300"></div>
|
||||
<span className="text-md mx-3 text-gray-500">or</span>
|
||||
<div className="h-px flex-1 bg-gray-300"></div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleSubmit} className="flex w-full flex-col gap-1">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
label="Email"
|
||||
placeholder="m@example.com"
|
||||
type="email"
|
||||
autoComplete="username"
|
||||
className="w-full"
|
||||
error={form.formState.errors.email?.message}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
label="Password"
|
||||
placeholder="•••••••••••••••••••••"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
error={form.formState.errors.password?.message}
|
||||
hint={
|
||||
<Link variant="secondary" href="/reset-password">
|
||||
Forgot password?
|
||||
</Link>
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-6 flex items-center">
|
||||
<div className="flex-1 border-t border-gray-300"></div>
|
||||
<span className="mx-3 text-sm text-gray-500">or</span>
|
||||
<div className="flex-1 border-t border-gray-300"></div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="m@example.com"
|
||||
{...field}
|
||||
type="email" // Explicitly specify email type
|
||||
autoComplete="username" // Added for password managers
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel className="flex w-full items-center justify-between">
|
||||
<span>Password</span>
|
||||
<Link
|
||||
href="/reset-password"
|
||||
className="text-sm font-normal leading-normal text-black underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput
|
||||
{...field}
|
||||
autoComplete="current-password" // Added for password managers
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Turnstile CAPTCHA Component */}
|
||||
<Turnstile
|
||||
key={captchaKey}
|
||||
siteKey={turnstile.siteKey}
|
||||
onVerify={turnstile.handleVerify}
|
||||
onExpire={turnstile.handleExpire}
|
||||
onError={turnstile.handleError}
|
||||
setWidgetId={turnstile.setWidgetId}
|
||||
action="login"
|
||||
shouldRender={turnstile.shouldRender}
|
||||
/>
|
||||
|
||||
{/* Turnstile CAPTCHA Component */}
|
||||
<Turnstile
|
||||
key={captchaKey}
|
||||
siteKey={turnstile.siteKey}
|
||||
onVerify={turnstile.handleVerify}
|
||||
onExpire={turnstile.handleExpire}
|
||||
onError={turnstile.handleError}
|
||||
setWidgetId={turnstile.setWidgetId}
|
||||
action="login"
|
||||
shouldRender={turnstile.shouldRender}
|
||||
<Button
|
||||
variant="primary"
|
||||
loading={isLoading}
|
||||
type="submit"
|
||||
className="mt-6 w-full"
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</form>
|
||||
<AuthFeedback
|
||||
type="login"
|
||||
message={feedback}
|
||||
isError={!!feedback}
|
||||
behaveAs={getBehaveAs()}
|
||||
/>
|
||||
|
||||
<AuthButton isLoading={isLoading} type="submit">
|
||||
Login
|
||||
</AuthButton>
|
||||
</form>
|
||||
<AuthFeedback
|
||||
type="login"
|
||||
message={feedback}
|
||||
isError={!!feedback}
|
||||
behaveAs={getBehaveAs()}
|
||||
</Form>
|
||||
<AuthCard.BottomText
|
||||
text="Don't have an account?"
|
||||
link={{ text: "Sign up", href: "/signup" }}
|
||||
/>
|
||||
</Form>
|
||||
<AuthBottomText
|
||||
text="Don't have an account?"
|
||||
linkText="Sign up"
|
||||
href="/signup"
|
||||
</AuthCard>
|
||||
<GoogleLoadingModal isOpen={isGoogleLoading} />
|
||||
<EmailNotAllowedModal
|
||||
isOpen={showNotAllowedModal}
|
||||
onClose={handleCloseNotAllowedModal}
|
||||
/>
|
||||
</AuthCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { useTurnstile } from "@/hooks/useTurnstile";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { BehaveAs, getBehaveAs } from "@/lib/utils";
|
||||
import { loginFormSchema, LoginProvider } from "@/types/auth";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { login, providerLogin } from "./actions";
|
||||
import z from "zod";
|
||||
import { BehaveAs } from "@/lib/utils";
|
||||
import { getBehaveAs } from "@/lib/utils";
|
||||
import { login, providerLogin } from "./actions";
|
||||
|
||||
export function useLoginPage() {
|
||||
const { supabase, user, isUserLoading } = useSupabase();
|
||||
const [feedback, setFeedback] = useState<string | null>(null);
|
||||
const [captchaKey, setCaptchaKey] = useState(0);
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
|
||||
const [showNotAllowedModal, setShowNotAllowedModal] = useState(false);
|
||||
const isCloudEnv = getBehaveAs() === BehaveAs.CLOUD;
|
||||
|
||||
const turnstile = useTurnstile({
|
||||
@@ -44,13 +46,22 @@ export function useLoginPage() {
|
||||
|
||||
async function handleProviderLogin(provider: LoginProvider) {
|
||||
setIsGoogleLoading(true);
|
||||
|
||||
// Artificially wait 2 seconds
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
try {
|
||||
const error = await providerLogin(provider);
|
||||
if (error) throw error;
|
||||
setFeedback(null);
|
||||
} catch (error) {
|
||||
resetCaptcha();
|
||||
setFeedback(JSON.stringify(error));
|
||||
const errorString = JSON.stringify(error);
|
||||
if (errorString.includes("not_allowed")) {
|
||||
setShowNotAllowedModal(true);
|
||||
} else {
|
||||
setFeedback(errorString);
|
||||
}
|
||||
} finally {
|
||||
setIsGoogleLoading(false);
|
||||
}
|
||||
@@ -59,14 +70,22 @@ export function useLoginPage() {
|
||||
async function handleLogin(data: z.infer<typeof loginFormSchema>) {
|
||||
setIsLoading(true);
|
||||
if (!turnstile.verified) {
|
||||
setFeedback("Please complete the CAPTCHA challenge.");
|
||||
toast({
|
||||
title: "Please complete the CAPTCHA challenge.",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
resetCaptcha();
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.email.includes("@agpt.co")) {
|
||||
setFeedback("Please use Google SSO to login using an AutoGPT email.");
|
||||
toast({
|
||||
title: "Please use Google SSO to login using an AutoGPT email.",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
resetCaptcha();
|
||||
return;
|
||||
@@ -76,7 +95,11 @@ export function useLoginPage() {
|
||||
await supabase?.auth.refreshSession();
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
setFeedback(error);
|
||||
toast({
|
||||
title: error,
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
resetCaptcha();
|
||||
// Always reset the turnstile on any error
|
||||
turnstile.reset();
|
||||
@@ -95,8 +118,10 @@ export function useLoginPage() {
|
||||
isCloudEnv,
|
||||
isUserLoading,
|
||||
isGoogleLoading,
|
||||
showNotAllowedModal,
|
||||
isSupabaseAvailable: !!supabase,
|
||||
handleSubmit: form.handleSubmit(handleLogin),
|
||||
handleProviderLogin,
|
||||
handleCloseNotAllowedModal: () => setShowNotAllowedModal(false),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import LoadingBox from "@/components/ui/loading";
|
||||
import { useToast, useToastOnFail } from "@/components/ui/use-toast";
|
||||
import useCredits from "@/hooks/useCredits";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { useSearchParams, useRouter } from "next/navigation";
|
||||
import { useToast, useToastOnFail } from "@/components/ui/use-toast";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
|
||||
import { RefundModal } from "./RefundModal";
|
||||
import { CreditTransaction } from "@/lib/autogpt-server-api";
|
||||
import { RefundModal } from "./RefundModal";
|
||||
|
||||
import {
|
||||
Table,
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
|
||||
export default function CreditsPage() {
|
||||
function CreditsContent() {
|
||||
const api = useBackendAPI();
|
||||
const {
|
||||
requestTopUp,
|
||||
@@ -371,3 +372,11 @@ export default function CreditsPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CreditsPage() {
|
||||
return (
|
||||
<Suspense fallback={<LoadingBox className="h-[80vh]" />}>
|
||||
<CreditsContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,43 +1,27 @@
|
||||
"use client";
|
||||
import {
|
||||
AuthButton,
|
||||
AuthCard,
|
||||
AuthFeedback,
|
||||
AuthHeader,
|
||||
PasswordInput,
|
||||
Turnstile,
|
||||
} from "@/components/auth";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { AuthCard } from "@/components/auth/AuthCard";
|
||||
import Turnstile from "@/components/auth/Turnstile";
|
||||
import { Form, FormField } from "@/components/ui/form";
|
||||
import LoadingBox from "@/components/ui/loading";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { useTurnstile } from "@/hooks/useTurnstile";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { getBehaveAs } from "@/lib/utils";
|
||||
import { changePasswordFormSchema, sendEmailFormSchema } from "@/types/auth";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Suspense, useCallback, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { changePassword, sendResetEmail } from "./actions";
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
function ResetPasswordContent() {
|
||||
const { supabase, user, isUserLoading } = useSupabase();
|
||||
const { toast } = useToast();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [feedback, setFeedback] = useState<string | null>(null);
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [sendEmailCaptchaKey, setSendEmailCaptchaKey] = useState(0);
|
||||
const [changePasswordCaptchaKey, setChangePasswordCaptchaKey] = useState(0);
|
||||
@@ -97,7 +81,6 @@ export default function ResetPasswordPage() {
|
||||
const onSendEmail = useCallback(
|
||||
async (data: z.infer<typeof sendEmailFormSchema>) => {
|
||||
setIsLoading(true);
|
||||
setFeedback(null);
|
||||
|
||||
if (!(await sendEmailForm.trigger())) {
|
||||
setIsLoading(false);
|
||||
@@ -105,8 +88,11 @@ export default function ResetPasswordPage() {
|
||||
}
|
||||
|
||||
if (!sendEmailTurnstile.verified) {
|
||||
setFeedback("Please complete the CAPTCHA challenge.");
|
||||
setIsError(true);
|
||||
toast({
|
||||
title: "CAPTCHA Required",
|
||||
description: "Please complete the CAPTCHA challenge.",
|
||||
variant: "destructive",
|
||||
});
|
||||
setIsLoading(false);
|
||||
resetSendEmailCaptcha();
|
||||
return;
|
||||
@@ -118,24 +104,28 @@ export default function ResetPasswordPage() {
|
||||
);
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
setFeedback(error);
|
||||
setIsError(true);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: error,
|
||||
variant: "destructive",
|
||||
});
|
||||
resetSendEmailCaptcha();
|
||||
return;
|
||||
}
|
||||
setDisabled(true);
|
||||
setFeedback(
|
||||
"Password reset email sent if user exists. Please check your email.",
|
||||
);
|
||||
setIsError(false);
|
||||
toast({
|
||||
title: "Email Sent",
|
||||
description:
|
||||
"Password reset email sent if user exists. Please check your email.",
|
||||
variant: "default",
|
||||
});
|
||||
},
|
||||
[sendEmailForm, sendEmailTurnstile, resetSendEmailCaptcha],
|
||||
[sendEmailForm, sendEmailTurnstile, resetSendEmailCaptcha, toast],
|
||||
);
|
||||
|
||||
const onChangePassword = useCallback(
|
||||
async (data: z.infer<typeof changePasswordFormSchema>) => {
|
||||
setIsLoading(true);
|
||||
setFeedback(null);
|
||||
|
||||
if (!(await changePasswordForm.trigger())) {
|
||||
setIsLoading(false);
|
||||
@@ -143,8 +133,11 @@ export default function ResetPasswordPage() {
|
||||
}
|
||||
|
||||
if (!changePasswordTurnstile.verified) {
|
||||
setFeedback("Please complete the CAPTCHA challenge.");
|
||||
setIsError(true);
|
||||
toast({
|
||||
title: "CAPTCHA Required",
|
||||
description: "Please complete the CAPTCHA challenge.",
|
||||
variant: "destructive",
|
||||
});
|
||||
setIsLoading(false);
|
||||
resetChangePasswordCaptcha();
|
||||
return;
|
||||
@@ -156,15 +149,26 @@ export default function ResetPasswordPage() {
|
||||
);
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
setFeedback(error);
|
||||
setIsError(true);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: error,
|
||||
variant: "destructive",
|
||||
});
|
||||
resetChangePasswordCaptcha();
|
||||
return;
|
||||
}
|
||||
setFeedback("Password changed successfully. Redirecting to login.");
|
||||
setIsError(false);
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Password changed successfully. Redirecting to login.",
|
||||
variant: "default",
|
||||
});
|
||||
},
|
||||
[changePasswordForm, changePasswordTurnstile, resetChangePasswordCaptcha],
|
||||
[
|
||||
changePasswordForm,
|
||||
changePasswordTurnstile,
|
||||
resetChangePasswordCaptcha,
|
||||
toast,
|
||||
],
|
||||
);
|
||||
|
||||
if (isUserLoading) {
|
||||
@@ -180,39 +184,43 @@ export default function ResetPasswordPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<AuthCard>
|
||||
<AuthHeader>Reset Password</AuthHeader>
|
||||
<div className="flex h-full min-h-[85vh] w-full flex-col items-center justify-center">
|
||||
<AuthCard title="Reset Password">
|
||||
{user ? (
|
||||
<form onSubmit={changePasswordForm.handleSubmit(onChangePassword)}>
|
||||
<form
|
||||
onSubmit={changePasswordForm.handleSubmit(onChangePassword)}
|
||||
className="flex w-full flex-col gap-1"
|
||||
>
|
||||
<Form {...changePasswordForm}>
|
||||
<FormField
|
||||
control={changePasswordForm.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
<Input
|
||||
label="Password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
error={
|
||||
changePasswordForm.formState.errors.password?.message
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={changePasswordForm.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Confirm Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
<FormDescription className="text-sm font-normal leading-tight text-slate-500">
|
||||
Password needs to be at least 12 characters long
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
<Input
|
||||
label="Confirm Password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
error={
|
||||
changePasswordForm.formState.errors.confirmPassword
|
||||
?.message
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -228,35 +236,34 @@ export default function ResetPasswordPage() {
|
||||
shouldRender={changePasswordTurnstile.shouldRender}
|
||||
/>
|
||||
|
||||
<AuthButton
|
||||
onClick={() => onChangePassword(changePasswordForm.getValues())}
|
||||
isLoading={isLoading}
|
||||
<Button
|
||||
variant="primary"
|
||||
loading={isLoading}
|
||||
type="submit"
|
||||
className="mt-6 w-full"
|
||||
onClick={() => onChangePassword(changePasswordForm.getValues())}
|
||||
>
|
||||
Update password
|
||||
</AuthButton>
|
||||
<AuthFeedback
|
||||
type="login"
|
||||
message={feedback}
|
||||
isError={isError}
|
||||
behaveAs={getBehaveAs()}
|
||||
/>
|
||||
</Button>
|
||||
</Form>
|
||||
</form>
|
||||
) : (
|
||||
<form onSubmit={sendEmailForm.handleSubmit(onSendEmail)}>
|
||||
<form
|
||||
onSubmit={sendEmailForm.handleSubmit(onSendEmail)}
|
||||
className="flex w-full flex-col gap-1"
|
||||
>
|
||||
<Form {...sendEmailForm}>
|
||||
<FormField
|
||||
control={sendEmailForm.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="m@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
<Input
|
||||
label="Email"
|
||||
placeholder="m@example.com"
|
||||
type="email"
|
||||
error={sendEmailForm.formState.errors.email?.message}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -272,20 +279,16 @@ export default function ResetPasswordPage() {
|
||||
shouldRender={sendEmailTurnstile.shouldRender}
|
||||
/>
|
||||
|
||||
<AuthButton
|
||||
onClick={() => onSendEmail(sendEmailForm.getValues())}
|
||||
isLoading={isLoading}
|
||||
<Button
|
||||
variant="primary"
|
||||
loading={isLoading}
|
||||
disabled={disabled}
|
||||
type="submit"
|
||||
className="mt-6 w-full"
|
||||
onClick={() => onSendEmail(sendEmailForm.getValues())}
|
||||
>
|
||||
Send reset email
|
||||
</AuthButton>
|
||||
<AuthFeedback
|
||||
type="login"
|
||||
message={feedback}
|
||||
isError={isError}
|
||||
behaveAs={getBehaveAs()}
|
||||
/>
|
||||
</Button>
|
||||
</Form>
|
||||
</form>
|
||||
)}
|
||||
@@ -293,3 +296,11 @@ export default function ResetPasswordPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
return (
|
||||
<Suspense fallback={<LoadingBox className="h-[80vh]" />}>
|
||||
<ResetPasswordContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { AuthCard } from "@/components/auth/AuthCard";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export function LoadingSignup() {
|
||||
return (
|
||||
<div className="flex h-full min-h-[85vh] flex-col items-center justify-center">
|
||||
<AuthCard title="">
|
||||
<Skeleton className="mx-auto h-8 w-48" />
|
||||
<Skeleton className="h-12 w-full rounded-md" />
|
||||
<div className="w-full space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-12" />
|
||||
<Skeleton className="h-12 w-full rounded-md" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-12 w-full rounded-md" />
|
||||
</div>
|
||||
<Skeleton className="h-16 w-full rounded-md" />
|
||||
<Skeleton className="h-12 w-full rounded-md" />
|
||||
<div className="flex justify-center space-x-1">
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<Skeleton className="h-4 w-12" />
|
||||
</div>
|
||||
</div>
|
||||
</AuthCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Link } from "@/components/atoms/Link/Link";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { AuthCard } from "@/components/auth/AuthCard";
|
||||
import AuthFeedback from "@/components/auth/AuthFeedback";
|
||||
import { EmailNotAllowedModal } from "@/components/auth/EmailNotAllowedModal";
|
||||
import { GoogleLoadingModal } from "@/components/auth/GoogleLoadingModal";
|
||||
import { GoogleOAuthButton } from "@/components/auth/GoogleOAuthButton";
|
||||
import Turnstile from "@/components/auth/Turnstile";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import Link from "next/link";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import LoadingBox from "@/components/ui/loading";
|
||||
import {
|
||||
AuthCard,
|
||||
AuthHeader,
|
||||
AuthButton,
|
||||
AuthBottomText,
|
||||
GoogleOAuthButton,
|
||||
PasswordInput,
|
||||
Turnstile,
|
||||
} from "@/components/auth";
|
||||
import AuthFeedback from "@/components/auth/AuthFeedback";
|
||||
import { getBehaveAs } from "@/lib/utils";
|
||||
import { WarningOctagonIcon } from "@phosphor-icons/react/dist/ssr";
|
||||
import { LoadingSignup } from "./components/LoadingSignup";
|
||||
import { useSignupPage } from "./useSignupPage";
|
||||
|
||||
export default function SignupPage() {
|
||||
@@ -36,13 +34,15 @@ export default function SignupPage() {
|
||||
isCloudEnv,
|
||||
isUserLoading,
|
||||
isGoogleLoading,
|
||||
showNotAllowedModal,
|
||||
isSupabaseAvailable,
|
||||
handleSubmit,
|
||||
handleProviderSignup,
|
||||
handleCloseNotAllowedModal,
|
||||
} = useSignupPage();
|
||||
|
||||
if (isUserLoading || isLoggedIn) {
|
||||
return <LoadingBox className="h-[80vh]" />;
|
||||
return <LoadingSignup />;
|
||||
}
|
||||
|
||||
if (!isSupabaseAvailable) {
|
||||
@@ -53,143 +53,167 @@ export default function SignupPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const confirmPasswordError = form.formState.errors.confirmPassword?.message;
|
||||
const termsError = form.formState.errors.agreeToTerms?.message;
|
||||
|
||||
return (
|
||||
<AuthCard className="mx-auto mt-12">
|
||||
<AuthHeader>Create a new account</AuthHeader>
|
||||
<div className="mt-10 flex h-full min-h-[85vh] flex-col items-center justify-center">
|
||||
<AuthCard title="Create a new account">
|
||||
{isCloudEnv ? (
|
||||
<>
|
||||
<div className="mb-2 w-full">
|
||||
<GoogleOAuthButton
|
||||
onClick={() => handleProviderSignup("google")}
|
||||
isLoading={isGoogleLoading}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 flex w-full items-center">
|
||||
<div className="h-px flex-1 bg-gray-300"></div>
|
||||
<span className="text-md mx-3 text-gray-500">or</span>
|
||||
<div className="h-px flex-1 bg-gray-300"></div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{isCloudEnv ? (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<GoogleOAuthButton
|
||||
onClick={() => handleProviderSignup("google")}
|
||||
isLoading={isGoogleLoading}
|
||||
disabled={isLoading}
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleSubmit} className="flex w-full flex-col gap-1">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
label="Email"
|
||||
placeholder="m@example.com"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
error={form.formState.errors.email?.message}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
label="Password"
|
||||
placeholder="********"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
error={form.formState.errors.password?.message}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
label="Confirm Password"
|
||||
placeholder="********"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
error={confirmPasswordError}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-6 flex items-center">
|
||||
<div className="flex-1 border-t border-gray-300"></div>
|
||||
<span className="mx-3 text-sm text-gray-500">or</span>
|
||||
<div className="flex-1 border-t border-gray-300"></div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="m@example.com"
|
||||
{...field}
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} autoComplete="new-password" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-4">
|
||||
<FormLabel>Confirm Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} autoComplete="new-password" />
|
||||
</FormControl>
|
||||
<FormDescription className="text-sm font-normal leading-tight text-slate-500">
|
||||
Password needs to be at least 12 characters long
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="agreeToTerms"
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<FormItem className="mt-6 flex w-full flex-row items-center -space-y-1 space-x-2">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
className="relative bottom-px"
|
||||
/>
|
||||
</FormControl>
|
||||
<div>
|
||||
<FormLabel className="flex flex-wrap items-center gap-1">
|
||||
<Text
|
||||
variant="body-medium"
|
||||
className="inline-block text-slate-950"
|
||||
>
|
||||
I agree to the
|
||||
</Text>
|
||||
<Link
|
||||
href="https://auto-gpt.notion.site/Terms-of-Use-11400ef5bece80d0b087d7831c5fd6bf"
|
||||
variant="secondary"
|
||||
>
|
||||
Terms of Use
|
||||
</Link>
|
||||
<Text
|
||||
variant="body-medium"
|
||||
className="inline-block text-slate-950"
|
||||
>
|
||||
and
|
||||
</Text>
|
||||
<Link
|
||||
href="https://www.notion.so/auto-gpt/Privacy-Policy-ab11c9c20dbd4de1a15dcffe84d77984"
|
||||
variant="secondary"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</FormLabel>
|
||||
</div>
|
||||
</FormItem>
|
||||
{termsError ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<WarningOctagonIcon className="h-4 w-4 text-red-500" />
|
||||
<Text variant="small-medium" className="!text-red-500">
|
||||
{termsError}
|
||||
</Text>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Turnstile CAPTCHA Component */}
|
||||
<Turnstile
|
||||
key={captchaKey}
|
||||
siteKey={turnstile.siteKey}
|
||||
onVerify={turnstile.handleVerify}
|
||||
onExpire={turnstile.handleExpire}
|
||||
onError={turnstile.handleError}
|
||||
setWidgetId={turnstile.setWidgetId}
|
||||
action="signup"
|
||||
shouldRender={turnstile.shouldRender}
|
||||
/>
|
||||
{/* Turnstile CAPTCHA Component */}
|
||||
<Turnstile
|
||||
key={captchaKey}
|
||||
siteKey={turnstile.siteKey}
|
||||
onVerify={turnstile.handleVerify}
|
||||
onExpire={turnstile.handleExpire}
|
||||
onError={turnstile.handleError}
|
||||
setWidgetId={turnstile.setWidgetId}
|
||||
action="signup"
|
||||
shouldRender={turnstile.shouldRender}
|
||||
/>
|
||||
|
||||
<AuthButton isLoading={isLoading} type="submit">
|
||||
Sign up
|
||||
</AuthButton>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="agreeToTerms"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-6 flex flex-row items-start -space-y-1 space-x-2">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="">
|
||||
<FormLabel>
|
||||
<span className="mr-1 text-sm font-normal leading-normal text-slate-950">
|
||||
I agree to the
|
||||
</span>
|
||||
<Link
|
||||
href="https://auto-gpt.notion.site/Terms-of-Use-11400ef5bece80d0b087d7831c5fd6bf"
|
||||
className="text-sm font-normal leading-normal text-slate-950 underline"
|
||||
>
|
||||
Terms of Use
|
||||
</Link>
|
||||
<span className="mx-1 text-sm font-normal leading-normal text-slate-950">
|
||||
and
|
||||
</span>
|
||||
<Link
|
||||
href="https://www.notion.so/auto-gpt/Privacy-Policy-ab11c9c20dbd4de1a15dcffe84d77984"
|
||||
className="text-sm font-normal leading-normal text-slate-950 underline"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</FormLabel>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
<AuthFeedback
|
||||
type="signup"
|
||||
message={feedback}
|
||||
isError={!!feedback}
|
||||
behaveAs={getBehaveAs()}
|
||||
<Button
|
||||
variant="primary"
|
||||
loading={isLoading}
|
||||
type="submit"
|
||||
className="mt-6 w-full"
|
||||
>
|
||||
Sign up
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
<AuthFeedback
|
||||
type="signup"
|
||||
message={feedback}
|
||||
isError={!!feedback}
|
||||
behaveAs={getBehaveAs()}
|
||||
/>
|
||||
|
||||
<AuthCard.BottomText
|
||||
text="Already a member?"
|
||||
link={{ text: "Log in", href: "/login" }}
|
||||
/>
|
||||
</AuthCard>
|
||||
<GoogleLoadingModal isOpen={isGoogleLoading} />
|
||||
<EmailNotAllowedModal
|
||||
isOpen={showNotAllowedModal}
|
||||
onClose={handleCloseNotAllowedModal}
|
||||
/>
|
||||
|
||||
<AuthBottomText
|
||||
text="Already a member?"
|
||||
linkText="Log in"
|
||||
href="/login"
|
||||
/>
|
||||
</AuthCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { useTurnstile } from "@/hooks/useTurnstile";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { signupFormSchema, LoginProvider } from "@/types/auth";
|
||||
import { BehaveAs, getBehaveAs } from "@/lib/utils";
|
||||
import { LoginProvider, signupFormSchema } from "@/types/auth";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { signup } from "./actions";
|
||||
import { providerLogin } from "../login/actions";
|
||||
import z from "zod";
|
||||
import { BehaveAs, getBehaveAs } from "@/lib/utils";
|
||||
import { providerLogin } from "../login/actions";
|
||||
import { signup } from "./actions";
|
||||
|
||||
export function useSignupPage() {
|
||||
const { supabase, user, isUserLoading } = useSupabase();
|
||||
const [feedback, setFeedback] = useState<string | null>(null);
|
||||
const [captchaKey, setCaptchaKey] = useState(0);
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
|
||||
const [showNotAllowedModal, setShowNotAllowedModal] = useState(false);
|
||||
|
||||
const isCloudEnv = getBehaveAs() === BehaveAs.CLOUD;
|
||||
|
||||
const turnstile = useTurnstile({
|
||||
@@ -50,7 +54,10 @@ export function useSignupPage() {
|
||||
setIsGoogleLoading(false);
|
||||
if (error) {
|
||||
resetCaptcha();
|
||||
setFeedback(error);
|
||||
toast({
|
||||
title: error,
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
setFeedback(null);
|
||||
@@ -60,16 +67,21 @@ export function useSignupPage() {
|
||||
setIsLoading(true);
|
||||
|
||||
if (!turnstile.verified) {
|
||||
setFeedback("Please complete the CAPTCHA challenge.");
|
||||
toast({
|
||||
title: "Please complete the CAPTCHA challenge.",
|
||||
variant: "default",
|
||||
});
|
||||
setIsLoading(false);
|
||||
resetCaptcha();
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.email.includes("@agpt.co")) {
|
||||
setFeedback(
|
||||
"Please use Google SSO to create an account using an AutoGPT email.",
|
||||
);
|
||||
toast({
|
||||
title:
|
||||
"Please use Google SSO to create an account using an AutoGPT email.",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
resetCaptcha();
|
||||
@@ -83,8 +95,13 @@ export function useSignupPage() {
|
||||
setFeedback("User with this email already exists");
|
||||
turnstile.reset();
|
||||
return;
|
||||
} else if (error === "not_allowed") {
|
||||
setShowNotAllowedModal(true);
|
||||
} else {
|
||||
setFeedback(error);
|
||||
toast({
|
||||
title: error,
|
||||
variant: "destructive",
|
||||
});
|
||||
resetCaptcha();
|
||||
turnstile.reset();
|
||||
}
|
||||
@@ -103,8 +120,10 @@ export function useSignupPage() {
|
||||
isCloudEnv,
|
||||
isUserLoading,
|
||||
isGoogleLoading,
|
||||
showNotAllowedModal,
|
||||
isSupabaseAvailable: !!supabase,
|
||||
handleSubmit: form.handleSubmit(handleSignup),
|
||||
handleCloseNotAllowedModal: () => setShowNotAllowedModal(false),
|
||||
handleProviderSignup,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { ThemeProviderProps } from "next-themes";
|
||||
import { BackendAPIProvider } from "@/lib/autogpt-server-api/context";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import CredentialsProvider from "@/components/integrations/credentials-provider";
|
||||
import { LaunchDarklyProvider } from "@/components/feature-flag/feature-flag-provider";
|
||||
import CredentialsProvider from "@/components/integrations/credentials-provider";
|
||||
import OnboardingProvider from "@/components/onboarding/onboarding-provider";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { BackendAPIProvider } from "@/lib/autogpt-server-api/context";
|
||||
import { getQueryClient } from "@/lib/react-query/queryClient";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import {
|
||||
ThemeProvider as NextThemesProvider,
|
||||
ThemeProviderProps,
|
||||
} from "next-themes";
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||
|
||||
export function Providers({ children, ...props }: ThemeProviderProps) {
|
||||
const queryClient = getQueryClient();
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NextThemesProvider {...props}>
|
||||
<BackendAPIProvider>
|
||||
<CredentialsProvider>
|
||||
<LaunchDarklyProvider>
|
||||
<OnboardingProvider>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
</OnboardingProvider>
|
||||
</LaunchDarklyProvider>
|
||||
</CredentialsProvider>
|
||||
</BackendAPIProvider>
|
||||
</NextThemesProvider>
|
||||
<NuqsAdapter>
|
||||
<NextThemesProvider {...props}>
|
||||
<BackendAPIProvider>
|
||||
<CredentialsProvider>
|
||||
<LaunchDarklyProvider>
|
||||
<OnboardingProvider>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
</OnboardingProvider>
|
||||
</LaunchDarklyProvider>
|
||||
</CredentialsProvider>
|
||||
</BackendAPIProvider>
|
||||
</NextThemesProvider>
|
||||
</NuqsAdapter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,34 +1,17 @@
|
||||
"use client";
|
||||
import React, {
|
||||
createContext,
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
MouseEvent,
|
||||
Suspense,
|
||||
} from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ReactFlow,
|
||||
ReactFlowProvider,
|
||||
Controls,
|
||||
Background,
|
||||
Node,
|
||||
OnConnect,
|
||||
Connection,
|
||||
MarkerType,
|
||||
NodeChange,
|
||||
EdgeChange,
|
||||
useReactFlow,
|
||||
applyEdgeChanges,
|
||||
applyNodeChanges,
|
||||
useViewport,
|
||||
} from "@xyflow/react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import { CustomNode } from "./CustomNode";
|
||||
import "./flow.css";
|
||||
import { CronSchedulerDialog } from "@/components/cron-scheduler-dialog";
|
||||
import { BlocksControl } from "@/components/edit/control/BlocksControl";
|
||||
import { Control, ControlPanel } from "@/components/edit/control/ControlPanel";
|
||||
import { SaveControl } from "@/components/edit/control/SaveControl";
|
||||
import OttoChatWidget from "@/components/OttoChatWidget";
|
||||
import PrimaryActionBar from "@/components/PrimaryActionButton";
|
||||
import RunnerUIWrapper, {
|
||||
RunnerUIWrapperRef,
|
||||
} from "@/components/RunnerUIWrapper";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { IconRedo2, IconUndo2 } from "@/components/ui/icons";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import useAgentGraph from "@/hooks/useAgentGraph";
|
||||
import {
|
||||
BlockUIType,
|
||||
formatEdgeID,
|
||||
@@ -37,27 +20,44 @@ import {
|
||||
LibraryAgent,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { getTypeColor, findNewlyAddedBlockCoordinates } from "@/lib/utils";
|
||||
import { history } from "./history";
|
||||
import { CustomEdge } from "./CustomEdge";
|
||||
import ConnectionLine from "./ConnectionLine";
|
||||
import { Control, ControlPanel } from "@/components/edit/control/ControlPanel";
|
||||
import { SaveControl } from "@/components/edit/control/SaveControl";
|
||||
import { BlocksControl } from "@/components/edit/control/BlocksControl";
|
||||
import { IconUndo2, IconRedo2 } from "@/components/ui/icons";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { startTutorial } from "./tutorial";
|
||||
import useAgentGraph from "@/hooks/useAgentGraph";
|
||||
import { findNewlyAddedBlockCoordinates, getTypeColor } from "@/lib/utils";
|
||||
import {
|
||||
applyEdgeChanges,
|
||||
applyNodeChanges,
|
||||
Background,
|
||||
Connection,
|
||||
Controls,
|
||||
EdgeChange,
|
||||
MarkerType,
|
||||
Node,
|
||||
NodeChange,
|
||||
OnConnect,
|
||||
ReactFlow,
|
||||
ReactFlowProvider,
|
||||
useReactFlow,
|
||||
useViewport,
|
||||
} from "@xyflow/react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import React, {
|
||||
createContext,
|
||||
MouseEvent,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { useRouter, usePathname, useSearchParams } from "next/navigation";
|
||||
import RunnerUIWrapper, {
|
||||
RunnerUIWrapperRef,
|
||||
} from "@/components/RunnerUIWrapper";
|
||||
import { CronSchedulerDialog } from "@/components/cron-scheduler-dialog";
|
||||
import PrimaryActionBar from "@/components/PrimaryActionButton";
|
||||
import OttoChatWidget from "@/components/OttoChatWidget";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { useCopyPaste } from "../hooks/useCopyPaste";
|
||||
import ConnectionLine from "./ConnectionLine";
|
||||
import { CustomEdge } from "./CustomEdge";
|
||||
import { CustomNode } from "./CustomNode";
|
||||
import "./flow.css";
|
||||
import { history } from "./history";
|
||||
import { startTutorial } from "./tutorial";
|
||||
|
||||
// This is for the history, this is the minimum distance a block must move before it is logged
|
||||
// It helps to prevent spamming the history with small movements especially when pressing on a input in a block
|
||||
@@ -84,7 +84,8 @@ const FlowEditor: React.FC<{
|
||||
flowID?: GraphID;
|
||||
flowVersion?: number;
|
||||
className?: string;
|
||||
}> = ({ flowID, flowVersion, className }) => {
|
||||
searchParams?: URLSearchParams;
|
||||
}> = ({ flowID, flowVersion, className, searchParams }) => {
|
||||
const {
|
||||
addNodes,
|
||||
addEdges,
|
||||
@@ -144,7 +145,7 @@ const FlowEditor: React.FC<{
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const params = useSearchParams();
|
||||
const params = searchParams || new URLSearchParams();
|
||||
const initialPositionRef = useRef<{
|
||||
[key: string]: { x: number; y: number };
|
||||
}>({});
|
||||
@@ -814,9 +815,26 @@ const FlowEditor: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const FlowEditorWithSearchParams: React.FC<{
|
||||
flowID?: GraphID;
|
||||
flowVersion?: number;
|
||||
className?: string;
|
||||
}> = (props) => {
|
||||
const searchParams = useSearchParams();
|
||||
return <FlowEditor {...props} searchParams={searchParams} />;
|
||||
};
|
||||
|
||||
const WrappedFlowEditor: typeof FlowEditor = (props) => (
|
||||
<ReactFlowProvider>
|
||||
<FlowEditor {...props} />
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
Loading...
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FlowEditorWithSearchParams {...props} />
|
||||
</Suspense>
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter, usePathname, useSearchParams } from "next/navigation";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Search } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -13,8 +10,11 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { SubmissionStatus } from "@/lib/autogpt-server-api/types";
|
||||
import { Search } from "lucide-react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
|
||||
export function SearchAndFilterAdminMarketplace({
|
||||
function SearchAndFilterAdminMarketplaceContent({
|
||||
initialSearch,
|
||||
}: {
|
||||
initialStatus?: SubmissionStatus;
|
||||
@@ -98,3 +98,20 @@ export function SearchAndFilterAdminMarketplace({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SearchAndFilterAdminMarketplace({
|
||||
initialSearch,
|
||||
}: {
|
||||
initialStatus?: SubmissionStatus;
|
||||
initialSearch?: string;
|
||||
}) {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex items-center justify-center">Loading...</div>
|
||||
}
|
||||
>
|
||||
<SearchAndFilterAdminMarketplaceContent initialSearch={initialSearch} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter, usePathname, useSearchParams } from "next/navigation";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Search } from "lucide-react";
|
||||
import { CreditTransactionType } from "@/lib/autogpt-server-api";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -13,8 +9,12 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { CreditTransactionType } from "@/lib/autogpt-server-api";
|
||||
import { Search } from "lucide-react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
|
||||
export function SearchAndFilterAdminSpending({
|
||||
function SearchAndFilterAdminSpendingContent({
|
||||
initialSearch,
|
||||
}: {
|
||||
initialStatus?: CreditTransactionType;
|
||||
@@ -102,3 +102,20 @@ export function SearchAndFilterAdminSpending({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SearchAndFilterAdminSpending({
|
||||
initialSearch,
|
||||
}: {
|
||||
initialStatus?: CreditTransactionType;
|
||||
initialSearch?: string;
|
||||
}) {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex items-center justify-center">Loading...</div>
|
||||
}
|
||||
>
|
||||
<SearchAndFilterAdminSpendingContent initialSearch={initialSearch} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"use client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
GraphExecutionID,
|
||||
GraphExecutionMeta,
|
||||
@@ -12,14 +11,15 @@ import {
|
||||
Schedule,
|
||||
ScheduleID,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Button } from "@/components/agptui/Button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
import { agentRunStatusMap } from "@/components/agents/agent-run-status-chip";
|
||||
import AgentRunSummaryCard from "@/components/agents/agent-run-summary-card";
|
||||
import { Button } from "../atoms/Button/Button";
|
||||
|
||||
interface AgentRunsSelectorListProps {
|
||||
agent: LibraryAgent;
|
||||
@@ -72,17 +72,11 @@ export default function AgentRunsSelectorList({
|
||||
<aside className={cn("flex flex-col gap-4", className)}>
|
||||
{allowDraftNewRun && (
|
||||
<Button
|
||||
size="card"
|
||||
className={
|
||||
"mb-4 hidden h-16 w-72 items-center gap-2 py-6 lg:flex xl:w-80 " +
|
||||
(selectedView.type == "run" && !selectedView.id
|
||||
? "agpt-card-selected text-accent"
|
||||
: "")
|
||||
}
|
||||
className={"mb-4 hidden lg:flex"}
|
||||
onClick={onSelectDraftNewRun}
|
||||
leftIcon={<Plus className="h-6 w-6" />}
|
||||
>
|
||||
<Plus className="h-6 w-6" />
|
||||
<span>New {agent.has_external_trigger ? "trigger" : "run"}</span>
|
||||
New {agent.has_external_trigger ? "trigger" : "run"}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -112,7 +106,7 @@ export default function AgentRunsSelectorList({
|
||||
{/* New Run button - only in small layouts */}
|
||||
{allowDraftNewRun && (
|
||||
<Button
|
||||
size="card"
|
||||
size="large"
|
||||
className={
|
||||
"flex h-28 w-40 items-center gap-2 py-6 lg:hidden " +
|
||||
(selectedView.type == "run" && !selectedView.id
|
||||
@@ -120,9 +114,9 @@ export default function AgentRunsSelectorList({
|
||||
: "")
|
||||
}
|
||||
onClick={onSelectDraftNewRun}
|
||||
leftIcon={<Plus className="h-6 w-6" />}
|
||||
>
|
||||
<Plus className="h-6 w-6" />
|
||||
<span>New {agent.has_external_trigger ? "trigger" : "run"}</span>
|
||||
New {agent.has_external_trigger ? "trigger" : "run"}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import * as React from "react";
|
||||
import Link from "next/link";
|
||||
import { ProfilePopoutMenu } from "./ProfilePopoutMenu";
|
||||
import { IconType, IconLogIn, IconAutoGPTLogo } from "@/components/ui/icons";
|
||||
import { MobileNavBar } from "./MobileNavBar";
|
||||
import { Button } from "./Button";
|
||||
import Wallet from "./Wallet";
|
||||
import { ProfileDetails } from "@/lib/autogpt-server-api/types";
|
||||
import { NavbarLink } from "./NavbarLink";
|
||||
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
import { getServerUser } from "@/lib/supabase/server/getServerUser";
|
||||
|
||||
// Disable theme toggle for now
|
||||
// import { ThemeToggle } from "./ThemeToggle";
|
||||
|
||||
interface NavLink {
|
||||
name: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
interface NavbarProps {
|
||||
links: NavLink[];
|
||||
menuItemGroups: {
|
||||
groupName?: string;
|
||||
items: {
|
||||
icon: IconType;
|
||||
text: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
|
||||
async function getProfileData() {
|
||||
const api = new BackendAPI();
|
||||
const profile = await Promise.resolve(api.getStoreProfile());
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => {
|
||||
const { user } = await getServerUser();
|
||||
const isLoggedIn = user !== null;
|
||||
let profile: ProfileDetails | null = null;
|
||||
if (isLoggedIn) {
|
||||
profile = await getProfileData();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="sticky top-0 z-40 mx-[16px] hidden h-16 items-center justify-between rounded-bl-2xl rounded-br-2xl border border-white/50 bg-white/5 py-3 pl-6 pr-3 backdrop-blur-[26px] dark:border-gray-700 dark:bg-gray-900 md:inline-flex">
|
||||
<div className="flex items-center gap-11">
|
||||
<div className="relative h-10 w-[88.87px]">
|
||||
<IconAutoGPTLogo className="h-full w-full" />
|
||||
</div>
|
||||
{links.map((link) => (
|
||||
<NavbarLink key={link.name} name={link.name} href={link.href} />
|
||||
))}
|
||||
</div>
|
||||
{/* Profile section */}
|
||||
<div className="flex items-center gap-4">
|
||||
{isLoggedIn ? (
|
||||
<div className="flex items-center gap-4">
|
||||
{profile && <Wallet />}
|
||||
<ProfilePopoutMenu
|
||||
menuItemGroups={menuItemGroups}
|
||||
userName={profile?.username}
|
||||
userEmail={profile?.name}
|
||||
avatarSrc={profile?.avatar_url}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Link href="/login">
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex items-center justify-end space-x-2"
|
||||
>
|
||||
<IconLogIn className="h-5 h-[48px] w-5" />
|
||||
<span>Log In</span>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
{/* <ThemeToggle /> */}
|
||||
</div>
|
||||
</nav>
|
||||
{/* Mobile Navbar - Adjust positioning */}
|
||||
<>
|
||||
{isLoggedIn ? (
|
||||
<div className="fixed right-4 top-4 z-50">
|
||||
<MobileNavBar
|
||||
userName={profile?.username}
|
||||
menuItemGroups={[
|
||||
{
|
||||
groupName: "Navigation",
|
||||
items: links.map((link) => ({
|
||||
icon:
|
||||
link.name === "Marketplace"
|
||||
? IconType.Marketplace
|
||||
: link.name === "Library"
|
||||
? IconType.Library
|
||||
: link.name === "Build"
|
||||
? IconType.Builder
|
||||
: link.name === "Monitor"
|
||||
? IconType.Library
|
||||
: IconType.LayoutDashboard,
|
||||
text: link.name,
|
||||
href: link.href,
|
||||
})),
|
||||
},
|
||||
...menuItemGroups,
|
||||
]}
|
||||
userEmail={profile?.name}
|
||||
avatarSrc={profile?.avatar_url}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
href="/login"
|
||||
className="fixed right-4 top-4 z-50 mt-4 inline-flex h-8 items-center justify-end rounded-lg pr-4 md:hidden"
|
||||
>
|
||||
<Button size="sm" className="flex items-center space-x-2">
|
||||
<IconLogIn className="h-5 w-5" />
|
||||
<span>Log In</span>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
IconShoppingCart,
|
||||
IconBoxes,
|
||||
IconLibrary,
|
||||
IconLaptop,
|
||||
} from "@/components/ui/icons";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
interface NavbarLinkProps {
|
||||
name: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export const NavbarLink = ({ name, href }: NavbarLinkProps) => {
|
||||
const pathname = usePathname();
|
||||
const parts = pathname.split("/");
|
||||
const activeLink = "/" + (parts.length > 2 ? parts[2] : parts[1]);
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
data-testid={`navbar-link-${name.toLowerCase()}`}
|
||||
className="font-poppins text-[20px] leading-[28px]"
|
||||
>
|
||||
<div
|
||||
className={`h-[48px] px-5 py-4 ${
|
||||
activeLink === href
|
||||
? "rounded-2xl bg-neutral-800 dark:bg-neutral-200"
|
||||
: ""
|
||||
} flex items-center justify-start gap-3`}
|
||||
>
|
||||
{href === "/marketplace" && (
|
||||
<IconShoppingCart
|
||||
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
|
||||
/>
|
||||
)}
|
||||
{href === "/build" && (
|
||||
<IconBoxes
|
||||
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
|
||||
/>
|
||||
)}
|
||||
{href === "/monitor" && (
|
||||
<IconLaptop
|
||||
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
|
||||
/>
|
||||
)}
|
||||
{href === "/library" && (
|
||||
<IconLibrary
|
||||
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`hidden font-poppins text-[20px] font-medium leading-[28px] lg:block ${
|
||||
activeLink === href
|
||||
? "text-neutral-50 dark:text-neutral-900"
|
||||
: "text-neutral-900 dark:text-neutral-50"
|
||||
}`}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -56,7 +56,7 @@ const meta: Meta<typeof Button> = {
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
type Story = StoryObj<typeof Button>;
|
||||
|
||||
// Basic variants
|
||||
export const Primary: Story = {
|
||||
|
||||
@@ -1,82 +1,78 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CircleNotchIcon } from "@phosphor-icons/react/dist/ssr";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import Link, { type LinkProps } from "next/link";
|
||||
import React from "react";
|
||||
import { ButtonProps, extendedButtonVariants } from "./helpers";
|
||||
|
||||
// Extended button variants based on our design system
|
||||
const extendedButtonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 font-['Geist'] leading-snug border",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary:
|
||||
"bg-zinc-800 border-zinc-800 text-white hover:bg-zinc-900 hover:border-zinc-900 rounded-full disabled:text-white disabled:bg-zinc-200 disabled:border-zinc-200 disabled:opacity-1",
|
||||
secondary:
|
||||
"bg-zinc-100 border-zinc-100 text-black hover:bg-zinc-300 hover:border-zinc-300 rounded-full disabled:text-zinc-300 disabled:bg-zinc-50 disabled:border-zinc-50 disabled:opacity-1",
|
||||
destructive:
|
||||
"bg-red-500 border-red-500 text-white hover:bg-red-600 hover:border-red-600 rounded-full disabled:text-white disabled:bg-zinc-200 disabled:border-zinc-200 disabled:opacity-1",
|
||||
outline:
|
||||
"bg-transparent border-zinc-700 text-black hover:bg-zinc-100 hover:border-zinc-700 rounded-full disabled:border-zinc-200 disabled:text-zinc-200 disabled:opacity-1",
|
||||
ghost:
|
||||
"bg-transparent border-transparent text-black hover:bg-zinc-50 hover:border-zinc-50 rounded-full disabled:text-zinc-200 disabled:opacity-1",
|
||||
icon: "bg-white text-black border border-zinc-600 hover:bg-zinc-100 rounded-[96px] disabled:opacity-1",
|
||||
},
|
||||
size: {
|
||||
small: "px-3 py-2 text-sm gap-1.5 h-[2.25rem]",
|
||||
large: "min-w-20 px-4 py-3 text-sm gap-2",
|
||||
icon: "p-3",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "primary",
|
||||
size: "large",
|
||||
},
|
||||
},
|
||||
);
|
||||
export function Button(props: ButtonProps) {
|
||||
const {
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
loading = false,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
children,
|
||||
as = "button",
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof extendedButtonVariants> {
|
||||
loading?: boolean;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
loading = false,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
children,
|
||||
disabled,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
const disabled = "disabled" in props ? props.disabled : false;
|
||||
const isDisabled = disabled;
|
||||
|
||||
const buttonContent = (
|
||||
<>
|
||||
{loading && (
|
||||
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
|
||||
)}
|
||||
{!loading && leftIcon}
|
||||
{children}
|
||||
{!loading && rightIcon}
|
||||
</>
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return variant === "ghost" ? (
|
||||
<button
|
||||
const loadingClassName =
|
||||
variant === "ghost"
|
||||
? cn(
|
||||
extendedButtonVariants({ variant, size, className }),
|
||||
"pointer-events-none",
|
||||
)
|
||||
: cn(
|
||||
extendedButtonVariants({ variant: "primary", size, className }),
|
||||
"pointer-events-none border-zinc-500 bg-zinc-500 text-white",
|
||||
);
|
||||
|
||||
return as === "NextLink" ? (
|
||||
<Link
|
||||
{...(restProps as LinkProps)}
|
||||
className={loadingClassName}
|
||||
aria-disabled="true"
|
||||
>
|
||||
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
|
||||
{children}
|
||||
</Link>
|
||||
) : (
|
||||
<button className={loadingClassName} disabled>
|
||||
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (as === "NextLink") {
|
||||
return (
|
||||
<Link
|
||||
{...(restProps as LinkProps)}
|
||||
className={cn(
|
||||
extendedButtonVariants({ variant, size, className }),
|
||||
"pointer-events-none",
|
||||
loading && "pointer-events-none",
|
||||
isDisabled && "pointer-events-none opacity-50",
|
||||
)}
|
||||
aria-disabled={isDisabled}
|
||||
>
|
||||
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
|
||||
{children}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className={cn(
|
||||
extendedButtonVariants({ variant: "primary", size, className }),
|
||||
"pointer-events-none border-zinc-500 bg-zinc-500 text-white",
|
||||
)}
|
||||
>
|
||||
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
|
||||
{children}
|
||||
</button>
|
||||
{buttonContent}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -87,18 +83,9 @@ function Button({
|
||||
loading && "pointer-events-none",
|
||||
)}
|
||||
disabled={isDisabled}
|
||||
{...props}
|
||||
{...(restProps as React.ButtonHTMLAttributes<HTMLButtonElement>)}
|
||||
>
|
||||
{loading && (
|
||||
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
|
||||
)}
|
||||
{!loading && leftIcon}
|
||||
{children}
|
||||
{!loading && rightIcon}
|
||||
{buttonContent}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, extendedButtonVariants };
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import { LinkProps } from "next/link";
|
||||
|
||||
// Extended button variants based on our design system
|
||||
export const extendedButtonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 font-sans leading-snug border",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary:
|
||||
"bg-zinc-800 border-zinc-800 text-white hover:bg-zinc-900 hover:border-zinc-900 rounded-full disabled:text-white disabled:bg-zinc-200 disabled:border-zinc-200 disabled:opacity-1",
|
||||
secondary:
|
||||
"bg-zinc-100 border-zinc-100 text-black hover:bg-zinc-300 hover:border-zinc-300 rounded-full disabled:text-zinc-300 disabled:bg-zinc-50 disabled:border-zinc-50 disabled:opacity-1",
|
||||
destructive:
|
||||
"bg-red-500 border-red-500 text-white hover:bg-red-600 hover:border-red-600 rounded-full disabled:text-white disabled:bg-zinc-200 disabled:border-zinc-200 disabled:opacity-1",
|
||||
outline:
|
||||
"bg-transparent border-zinc-700 text-black hover:bg-zinc-100 hover:border-zinc-700 rounded-full disabled:border-zinc-200 disabled:text-zinc-200 disabled:opacity-1",
|
||||
ghost:
|
||||
"bg-transparent border-transparent text-black hover:bg-zinc-50 hover:border-zinc-50 rounded-full disabled:text-zinc-200 disabled:opacity-1",
|
||||
icon: "bg-white text-black border border-zinc-600 hover:bg-zinc-100 rounded-[96px] disabled:opacity-1",
|
||||
},
|
||||
size: {
|
||||
small: "px-3 py-2 text-sm gap-1.5 h-[2.25rem]",
|
||||
large: "min-w-20 px-4 py-3 text-sm gap-2",
|
||||
icon: "p-3",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "primary",
|
||||
size: "large",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
type BaseButtonProps = {
|
||||
loading?: boolean;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
asChild?: boolean;
|
||||
} & VariantProps<typeof extendedButtonVariants>;
|
||||
|
||||
type ButtonAsButton = BaseButtonProps &
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
as?: "button";
|
||||
href?: never;
|
||||
};
|
||||
|
||||
type ButtonAsLink = BaseButtonProps &
|
||||
Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof LinkProps> &
|
||||
LinkProps & {
|
||||
as: "NextLink";
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export type ButtonProps = ButtonAsButton | ButtonAsLink;
|
||||
15
autogpt_platform/frontend/src/components/atoms/Card/Card.tsx
Normal file
15
autogpt_platform/frontend/src/components/atoms/Card/Card.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Card({ children, className }: Props) {
|
||||
return (
|
||||
<div className={cn("rounded-large bg-white p-6 shadow-md", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Input as BaseInput, type InputProps } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ReactNode } from "react";
|
||||
import { Text } from "../Text/Text";
|
||||
import { useInput } from "./useInput";
|
||||
|
||||
@@ -8,6 +9,7 @@ export interface TextFieldProps extends InputProps {
|
||||
hideLabel?: boolean;
|
||||
decimalCount?: number; // Only used for type="amount"
|
||||
error?: string;
|
||||
hint?: ReactNode;
|
||||
}
|
||||
|
||||
export function Input({
|
||||
@@ -16,6 +18,7 @@ export function Input({
|
||||
placeholder,
|
||||
hideLabel = false,
|
||||
decimalCount,
|
||||
hint,
|
||||
error,
|
||||
...props
|
||||
}: TextFieldProps) {
|
||||
@@ -26,13 +29,13 @@ export function Input({
|
||||
className={cn(
|
||||
// Override the default input styles with Figma design
|
||||
"h-[2.875rem] rounded-3xl border border-zinc-200 bg-white px-4 py-2.5 shadow-none",
|
||||
"font-normal leading-6 text-black",
|
||||
"font-normal text-black",
|
||||
"placeholder:font-normal placeholder:text-zinc-400",
|
||||
// Focus and hover states
|
||||
"focus:border-zinc-400 focus:shadow-none focus:outline-none focus:ring-1 focus:ring-zinc-400 focus:ring-offset-0",
|
||||
// Error state
|
||||
error &&
|
||||
"border-2 border-red-500 focus:border-red-500 focus:ring-red-500",
|
||||
"border-1.5 border-red-500 focus:border-red-500 focus:ring-red-500",
|
||||
className,
|
||||
)}
|
||||
type={props.type}
|
||||
@@ -44,13 +47,19 @@ export function Input({
|
||||
);
|
||||
|
||||
const inputWithError = (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="relative mb-6">
|
||||
{input}
|
||||
{error && (
|
||||
<Text variant="small-medium" as="span" className="!text-red-500">
|
||||
{error}
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
variant="small-medium"
|
||||
as="span"
|
||||
className={cn(
|
||||
"absolute left-0 top-full mt-1 !text-red-500 transition-opacity duration-200",
|
||||
error ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
>
|
||||
{error || " "}{" "}
|
||||
{/* Always render with space to maintain consistent height calculation */}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -58,9 +67,12 @@ export function Input({
|
||||
inputWithError
|
||||
) : (
|
||||
<label className="flex flex-col gap-2">
|
||||
<Text variant="body-medium" as="span" className="text-black">
|
||||
{label}
|
||||
</Text>
|
||||
<div className="flex items-center justify-between">
|
||||
<Text variant="body-medium" as="span" className="text-black">
|
||||
{label}
|
||||
</Text>
|
||||
{hint}
|
||||
</div>
|
||||
{inputWithError}
|
||||
</label>
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
|
||||
) {
|
||||
const linkClasses = cn(
|
||||
// Base styles from Figma
|
||||
"font-['Geist'] text-sm font-medium leading-[22px] text-[var(--AutoGPT-Text-text-black,#141414)]",
|
||||
"font-sans text-sm font-medium leading-[22px] text-[var(--AutoGPT-Text-text-black,#141414)]",
|
||||
// Variant-specific underline styles
|
||||
variant === "primary" && "hover:underline",
|
||||
variant === "secondary" && "underline",
|
||||
|
||||
@@ -94,8 +94,6 @@ export function AllVariants() {
|
||||
code={`<Text variant="small">Small</Text>
|
||||
<Text variant="small-medium">Small Medium</Text>`}
|
||||
/>
|
||||
<Text variant="subtle">Subtle</Text>
|
||||
<StoryCode code={`<Text variant="subtle">Subtle</Text>`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -128,7 +126,6 @@ export function BodyText() {
|
||||
<Text variant="body-medium">Body Medium</Text>
|
||||
<Text variant="small">Small</Text>
|
||||
<Text variant="small-medium">Small Medium</Text>
|
||||
<Text variant="subtle">Subtle</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,24 +14,24 @@ export type As =
|
||||
|
||||
export const variants = {
|
||||
// Headings
|
||||
h1: "font-poppins text-5xl font-semibold leading-[56px] text-zinc-800",
|
||||
h2: "font-poppins text-4xl font-normal leading-[52px] text-zinc-800",
|
||||
h3: "font-poppins text-3xl font-medium leading-10 text-zinc-800",
|
||||
h4: "font-poppins text-base font-medium leading-normal text-zinc-800",
|
||||
h1: "font-poppins text-[2.75rem] font-semibold leading-[3.5rem] text-zinc-800",
|
||||
h2: "font-poppins text-[2.25rem] font-normal leading-[3.25rem] text-zinc-800",
|
||||
h3: "font-poppins text-[1.75rem] font-medium leading-[2.5rem] text-zinc-800",
|
||||
h4: "font-poppins text-[1rem] font-medium leading-[1.5rem] text-zinc-800",
|
||||
|
||||
// Body Text
|
||||
lead: "font-sans text-xl font-normal leading-loose text-muted-zinc-800",
|
||||
large: "font-sans text-base font-normal leading-normal text-zinc-800",
|
||||
lead: "font-sans text-[1.25rem] font-normal leading-[2rem] text-zinc-800",
|
||||
large: "font-sans text-[1rem] font-normal leading-[1.5rem] text-zinc-800",
|
||||
"large-medium":
|
||||
"font-sans text-base font-medium leading-normal text-zinc-800",
|
||||
"font-sans text-[1rem] font-medium leading-[1.5rem] text-zinc-800",
|
||||
"large-semibold":
|
||||
"font-sans text-base font-semibold leading-normal text-zinc-800",
|
||||
body: "font-sans text-sm font-normal leading-snug text-zinc-800",
|
||||
"font-sans text-[1rem] font-semibold leading-[1.5rem] text-zinc-800",
|
||||
body: "font-sans text-[0.875rem] font-normal leading-[1.375rem] text-zinc-800",
|
||||
"body-medium": "font-sans text-sm font-medium leading-snug text-zinc-800",
|
||||
small: "font-sans text-xs font-normal leading-tight text-zinc-800",
|
||||
"small-medium": "font-sans text-xs font-medium leading-tight text-zinc-800",
|
||||
subtle:
|
||||
"font-sans text-xs font-medium uppercase leading-tight tracking-wide text-zinc-800",
|
||||
small:
|
||||
"font-sans text-[0.75rem] font-normal leading-[1.125rem] text-zinc-800",
|
||||
"small-medium":
|
||||
"font-sans text-[0.75rem] font-medium leading-[1.125rem] text-zinc-800",
|
||||
} as const;
|
||||
|
||||
export type Variant = keyof typeof variants;
|
||||
@@ -49,5 +49,4 @@ export const variantElementMap: Record<Variant, As> = {
|
||||
"body-medium": "p",
|
||||
small: "p",
|
||||
"small-medium": "p",
|
||||
subtle: "p",
|
||||
};
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
text: string;
|
||||
linkText?: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export default function AuthBottomText({
|
||||
className = "",
|
||||
text,
|
||||
linkText,
|
||||
href = "",
|
||||
}: Props) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
className,
|
||||
"mt-8 inline-flex w-full items-center justify-center",
|
||||
)}
|
||||
>
|
||||
<span className="text-sm font-medium leading-normal text-slate-950">
|
||||
{text}
|
||||
</span>
|
||||
{linkText && (
|
||||
<Link
|
||||
href={href}
|
||||
className="ml-1 text-sm font-medium leading-normal text-slate-950 underline"
|
||||
>
|
||||
{linkText}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
import { FaSpinner } from "react-icons/fa";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode;
|
||||
isLoading?: boolean;
|
||||
disabled?: boolean;
|
||||
type?: "button" | "submit" | "reset";
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export default function AuthButton({
|
||||
children,
|
||||
isLoading = false,
|
||||
disabled = false,
|
||||
type = "button",
|
||||
onClick,
|
||||
}: Props) {
|
||||
return (
|
||||
<Button
|
||||
className="mt-2 w-full px-4 py-2 text-zinc-800"
|
||||
variant="outline"
|
||||
type={type}
|
||||
disabled={isLoading || disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{isLoading ? (
|
||||
<FaSpinner className="animate-spin" />
|
||||
) : (
|
||||
<div className="text-sm font-medium">{children}</div>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +1,52 @@
|
||||
import { ReactNode } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ReactNode } from "react";
|
||||
import { Card } from "../atoms/Card/Card";
|
||||
import { Link } from "../atoms/Link/Link";
|
||||
import { Text } from "../atoms/Text/Text";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
interface BottomTextProps {
|
||||
text: string;
|
||||
link?: { text: string; href: string };
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function AuthCard({ children, className }: Props) {
|
||||
AuthCard.BottomText = function BottomText({
|
||||
text,
|
||||
link,
|
||||
className,
|
||||
}: BottomTextProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-[80vh] w-[32rem] items-center justify-center",
|
||||
className,
|
||||
"mt-4 inline-flex w-full items-center justify-center gap-1",
|
||||
)}
|
||||
>
|
||||
<div className="w-full max-w-md rounded-lg bg-white p-6 shadow-md">
|
||||
{children}
|
||||
</div>
|
||||
<Text variant="body-medium" className="text-slate-950">
|
||||
{text}
|
||||
</Text>
|
||||
{link ? (
|
||||
<Link href={link.href} variant="secondary">
|
||||
{link.text}
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
title: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function AuthCard({ children, title }: Props) {
|
||||
return (
|
||||
<Card className="mx-auto flex min-h-[40vh] w-full max-w-[32rem] flex-col items-center justify-center gap-8">
|
||||
<Text variant="h3" as="h2" className="mb-3">
|
||||
{title}
|
||||
</Text>
|
||||
{children}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AlertCircle, CheckCircle } from "lucide-react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { HelpItem } from "@/components/auth/help-item";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { BehaveAs } from "@/lib/utils";
|
||||
import { AlertCircle, CheckCircle } from "lucide-react";
|
||||
|
||||
interface Props {
|
||||
type: "login" | "signup";
|
||||
@@ -16,12 +16,24 @@ export default function AuthFeedback({
|
||||
isError = false,
|
||||
behaveAs = BehaveAs.CLOUD,
|
||||
}: Props) {
|
||||
// If there's no message but isError is true, show a default error message
|
||||
const displayMessage =
|
||||
message || (isError ? "Something went wrong. Please try again." : "");
|
||||
|
||||
const isCloudMode = behaveAs === BehaveAs.CLOUD;
|
||||
const isLocalMode = behaveAs === BehaveAs.LOCAL;
|
||||
const showCloudHelp = isError && isCloudMode;
|
||||
const showLocalHelp = isError && isLocalMode;
|
||||
const isSignupFlow = type === "signup";
|
||||
const hasAnyHelpContent = showCloudHelp || showLocalHelp;
|
||||
|
||||
const hasContent = displayMessage || hasAnyHelpContent;
|
||||
|
||||
if (!hasContent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="mt-4 w-full space-y-4">
|
||||
{/* Message feedback */}
|
||||
{displayMessage && (
|
||||
<div className="text-center text-sm font-medium leading-normal">
|
||||
@@ -40,9 +52,8 @@ export default function AuthFeedback({
|
||||
)}
|
||||
|
||||
{/* Cloud-specific help */}
|
||||
{isError &&
|
||||
behaveAs === BehaveAs.CLOUD &&
|
||||
(type === "signup" ? (
|
||||
{showCloudHelp &&
|
||||
(isSignupFlow ? (
|
||||
<Card className="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm">
|
||||
<CardContent className="p-0">
|
||||
<div className="divide-y divide-slate-100">
|
||||
@@ -81,32 +92,30 @@ export default function AuthFeedback({
|
||||
))}
|
||||
|
||||
{/* Local-specific help */}
|
||||
{isError && behaveAs === BehaveAs.LOCAL && (
|
||||
<Card className="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm">
|
||||
<CardContent className="p-0">
|
||||
<div className="space-y-4 divide-y divide-slate-100">
|
||||
<HelpItem
|
||||
title="Having trouble getting AutoGPT running locally?"
|
||||
description="Ask for help on our"
|
||||
linkText="Discord"
|
||||
href="https://discord.gg/autogpt"
|
||||
/>
|
||||
{showLocalHelp && (
|
||||
<Card className="w-full overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm">
|
||||
<div className="w-full divide-y divide-slate-100">
|
||||
<HelpItem
|
||||
title="Having trouble getting AutoGPT running locally?"
|
||||
description="Ask for help on our"
|
||||
linkText="Discord"
|
||||
href="https://discord.gg/autogpt"
|
||||
/>
|
||||
|
||||
<HelpItem
|
||||
title="Think you've found a bug?"
|
||||
description="Open an issue on our"
|
||||
linkText="GitHub"
|
||||
href="https://github.com/Significant-Gravitas/AutoGPT"
|
||||
/>
|
||||
<HelpItem
|
||||
title="Think you've found a bug?"
|
||||
description="Open an issue on our"
|
||||
linkText="GitHub"
|
||||
href="https://github.com/Significant-Gravitas/AutoGPT"
|
||||
/>
|
||||
|
||||
<HelpItem
|
||||
title="Interested in the cloud-hosted version?"
|
||||
description="Join our"
|
||||
linkText="waitlist here"
|
||||
href="https://agpt.co/waitlist"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<HelpItem
|
||||
title="Interested in the cloud-hosted version?"
|
||||
description="Join our"
|
||||
linkText="waitlist here"
|
||||
href="https://agpt.co/waitlist"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function AuthHeader({ children }: Props) {
|
||||
return (
|
||||
<div className="mb-8 text-2xl font-semibold leading-normal text-slate-950">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Button } from "../atoms/Button/Button";
|
||||
import { Text } from "../atoms/Text/Text";
|
||||
import { Dialog } from "../molecules/Dialog/Dialog";
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function EmailNotAllowedModal({ isOpen, onClose }: Props) {
|
||||
return (
|
||||
<Dialog
|
||||
controlled={{ isOpen, set: onClose }}
|
||||
styling={{ maxWidth: "35rem" }}
|
||||
>
|
||||
<Dialog.Content>
|
||||
<div className="flex flex-col items-center gap-8 py-4">
|
||||
<Text variant="h3">Access Restricted</Text>
|
||||
<Text variant="large-medium" className="text-center">
|
||||
We're currently in a limited access phase. Your email address
|
||||
isn't on our current allowlist for early access. If you believe
|
||||
this is an error or would like to request access, please contact our
|
||||
support team.
|
||||
</Text>
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button variant="primary" onClick={onClose}>
|
||||
I understand
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { CircleNotchIcon } from "@phosphor-icons/react/dist/ssr";
|
||||
import Image from "next/image";
|
||||
import { Text } from "../atoms/Text/Text";
|
||||
import { Dialog } from "../molecules/Dialog/Dialog";
|
||||
|
||||
interface GoogleLoadingModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export function GoogleLoadingModal({ isOpen }: GoogleLoadingModalProps) {
|
||||
return (
|
||||
<Dialog forceOpen={isOpen} styling={{ maxWidth: "32rem" }}>
|
||||
<Dialog.Content>
|
||||
<div className="flex flex-col items-center gap-8 py-4">
|
||||
<div className="mb-2 flex items-center justify-center gap-3">
|
||||
<Image src="/google-logo.svg" alt="Google" width={20} height={20} />
|
||||
<Text variant="h3">Signing in with Google</Text>
|
||||
</div>
|
||||
<CircleNotchIcon
|
||||
className="h-10 w-10 animate-spin"
|
||||
weight="regular"
|
||||
/>
|
||||
<Text variant="large-medium" className="text-center">
|
||||
You're being redirected to Google to complete the sign-in
|
||||
process.
|
||||
<br /> Please don't close this tab or navigate away from this
|
||||
page.
|
||||
</Text>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FaGoogle, FaSpinner } from "react-icons/fa";
|
||||
import { Button } from "../ui/button";
|
||||
import Image from "next/image";
|
||||
import { Button } from "../atoms/Button/Button";
|
||||
|
||||
interface GoogleOAuthButtonProps {
|
||||
onClick: () => void;
|
||||
@@ -7,26 +7,22 @@ interface GoogleOAuthButtonProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function GoogleOAuthButton({
|
||||
export function GoogleOAuthButton({
|
||||
onClick,
|
||||
isLoading = false,
|
||||
disabled = false,
|
||||
isLoading,
|
||||
disabled,
|
||||
}: GoogleOAuthButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
className="w-full border bg-zinc-700 py-2 text-white disabled:opacity-50"
|
||||
disabled={isLoading || disabled}
|
||||
variant="secondary"
|
||||
className="w-full gap-3"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
loading={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<FaSpinner className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<FaGoogle className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
<span className="text-sm font-medium">
|
||||
{isLoading ? "Signing in..." : "Continue with Google"}
|
||||
</span>
|
||||
<Image src="/google-logo.svg" alt="Google" width={20} height={20} />
|
||||
{isLoading ? "Connecting..." : "Continue with Google"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import AuthBottomText from "./AuthBottomText";
|
||||
import AuthButton from "./AuthButton";
|
||||
import AuthCard from "./AuthCard";
|
||||
import AuthFeedback from "./AuthFeedback";
|
||||
import AuthHeader from "./AuthHeader";
|
||||
import GoogleOAuthButton from "./GoogleOAuthButton";
|
||||
import { PasswordInput } from "./PasswordInput";
|
||||
import Turnstile from "./Turnstile";
|
||||
|
||||
export {
|
||||
AuthBottomText,
|
||||
AuthButton,
|
||||
AuthCard,
|
||||
AuthFeedback,
|
||||
AuthHeader,
|
||||
GoogleOAuthButton,
|
||||
PasswordInput,
|
||||
Turnstile,
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import { IconAutoGPTLogo, IconType } from "@/components/ui/icons";
|
||||
import Wallet from "../../agptui/Wallet";
|
||||
import { AccountMenu } from "./components/AccountMenu/AccountMenu";
|
||||
import { AgentNotifications } from "./components/AgentNotifications/AgentNotifications";
|
||||
import { LoginButton } from "./components/LoginButton";
|
||||
import { MobileNavBar } from "./components/MobileNavbar/MobileNavBar";
|
||||
import { NavbarLink } from "./components/NavbarLink";
|
||||
import { NavbarLoading } from "./components/NavbarLoading";
|
||||
import { accountMenuItems, loggedInLinks, loggedOutLinks } from "./helpers";
|
||||
import { useNavbar } from "./useNavbar";
|
||||
|
||||
export function Navbar() {
|
||||
const { isLoggedIn, profile, isLoading } = useNavbar();
|
||||
|
||||
if (isLoading) {
|
||||
return <NavbarLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="sticky top-0 z-40 hidden h-16 items-center rounded-bl-2xl rounded-br-2xl border border-white/50 bg-white/5 p-3 backdrop-blur-[26px] md:inline-flex">
|
||||
{/* Left section */}
|
||||
<div className="flex flex-1 items-center gap-6">
|
||||
{isLoggedIn
|
||||
? loggedInLinks.map((link) => (
|
||||
<NavbarLink key={link.name} name={link.name} href={link.href} />
|
||||
))
|
||||
: loggedOutLinks.map((link) => (
|
||||
<NavbarLink key={link.name} name={link.name} href={link.href} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Centered logo */}
|
||||
<div className="absolute left-1/2 top-1/2 h-10 w-[88.87px] -translate-x-1/2 -translate-y-1/2">
|
||||
<IconAutoGPTLogo className="h-full w-full" />
|
||||
</div>
|
||||
|
||||
{/* Right section */}
|
||||
<div className="flex flex-1 items-center justify-end gap-4">
|
||||
{isLoggedIn ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<AgentNotifications />
|
||||
{profile && <Wallet />}
|
||||
<AccountMenu
|
||||
userName={profile?.username}
|
||||
userEmail={profile?.name}
|
||||
avatarSrc={profile?.avatar_url ?? ""}
|
||||
menuItemGroups={accountMenuItems}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<LoginButton />
|
||||
)}
|
||||
{/* <ThemeToggle /> */}
|
||||
</div>
|
||||
</nav>
|
||||
{/* Mobile Navbar - Adjust positioning */}
|
||||
<>
|
||||
{isLoggedIn ? (
|
||||
<div className="fixed right-4 top-4 z-50">
|
||||
<MobileNavBar
|
||||
userName={profile?.username}
|
||||
menuItemGroups={[
|
||||
{
|
||||
groupName: "Navigation",
|
||||
items: loggedInLinks.map((link) => ({
|
||||
icon:
|
||||
link.name === "Marketplace"
|
||||
? IconType.Marketplace
|
||||
: link.name === "Library"
|
||||
? IconType.Library
|
||||
: link.name === "Build"
|
||||
? IconType.Builder
|
||||
: link.name === "Monitor"
|
||||
? IconType.Library
|
||||
: IconType.LayoutDashboard,
|
||||
text: link.name,
|
||||
href: link.href,
|
||||
})),
|
||||
},
|
||||
...accountMenuItems,
|
||||
]}
|
||||
userEmail={profile?.name}
|
||||
avatarSrc={profile?.avatar_url ?? ""}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,74 +1,31 @@
|
||||
import * as React from "react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
IconType,
|
||||
IconEdit,
|
||||
IconLayoutDashboard,
|
||||
IconUploadCloud,
|
||||
IconSettings,
|
||||
IconLogOut,
|
||||
IconRefresh,
|
||||
IconMarketplace,
|
||||
IconLibrary,
|
||||
IconBuilder,
|
||||
} from "../ui/icons";
|
||||
import Link from "next/link";
|
||||
import { ProfilePopoutMenuLogoutButton } from "./ProfilePopoutMenuLogoutButton";
|
||||
import { PublishAgentPopout } from "./composite/PublishAgentPopout";
|
||||
import * as React from "react";
|
||||
import { ProfilePopoutMenuLogoutButton } from "../../../../agptui/ProfilePopoutMenuLogoutButton";
|
||||
import { PublishAgentPopout } from "../../../../agptui/composite/PublishAgentPopout";
|
||||
import { getAccountMenuOptionIcon, MenuItemGroup } from "../../helpers";
|
||||
|
||||
interface ProfilePopoutMenuProps {
|
||||
interface Props {
|
||||
userName?: string;
|
||||
userEmail?: string;
|
||||
avatarSrc?: string;
|
||||
hideNavBarUsername?: boolean;
|
||||
menuItemGroups: {
|
||||
groupName?: string;
|
||||
items: {
|
||||
icon: IconType;
|
||||
text: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
}[];
|
||||
}[];
|
||||
menuItemGroups: MenuItemGroup[];
|
||||
}
|
||||
|
||||
export function ProfilePopoutMenu({
|
||||
export function AccountMenu({
|
||||
userName,
|
||||
userEmail,
|
||||
avatarSrc,
|
||||
menuItemGroups,
|
||||
}: ProfilePopoutMenuProps) {
|
||||
}: Props) {
|
||||
const popupId = React.useId();
|
||||
|
||||
const getIcon = (icon: IconType) => {
|
||||
const iconClass = "w-6 h-6";
|
||||
switch (icon) {
|
||||
case IconType.LayoutDashboard:
|
||||
return <IconLayoutDashboard className={iconClass} />;
|
||||
case IconType.UploadCloud:
|
||||
return <IconUploadCloud className={iconClass} />;
|
||||
case IconType.Edit:
|
||||
return <IconEdit className={iconClass} />;
|
||||
case IconType.Settings:
|
||||
return <IconSettings className={iconClass} />;
|
||||
case IconType.LogOut:
|
||||
return <IconLogOut className={iconClass} />;
|
||||
case IconType.Marketplace:
|
||||
return <IconMarketplace className={iconClass} />;
|
||||
case IconType.Library:
|
||||
return <IconLibrary className={iconClass} />;
|
||||
case IconType.Builder:
|
||||
return <IconBuilder className={iconClass} />;
|
||||
default:
|
||||
return <IconRefresh className={iconClass} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
@@ -127,7 +84,7 @@ export function ProfilePopoutMenu({
|
||||
className="inline-flex w-full items-center justify-start gap-2.5"
|
||||
>
|
||||
<div className="relative h-6 w-6">
|
||||
{getIcon(item.icon)}
|
||||
{getAccountMenuOptionIcon(item.icon)}
|
||||
</div>
|
||||
<div className="font-sans text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
{item.text}
|
||||
@@ -143,7 +100,7 @@ export function ProfilePopoutMenu({
|
||||
trigger={
|
||||
<div className="inline-flex w-full items-center justify-start gap-2.5">
|
||||
<div className="relative h-6 w-6">
|
||||
{getIcon(item.icon)}
|
||||
{getAccountMenuOptionIcon(item.icon)}
|
||||
</div>
|
||||
<div className="font-sans text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
{item.text}
|
||||
@@ -165,7 +122,7 @@ export function ProfilePopoutMenu({
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="relative h-6 w-6">
|
||||
{getIcon(item.icon)}
|
||||
{getAccountMenuOptionIcon(item.icon)}
|
||||
</div>
|
||||
<div className="font-sans text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
|
||||
{item.text}
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Bell } from "@phosphor-icons/react";
|
||||
import { useState } from "react";
|
||||
import { NotificationDropdown } from "./components/NotificationDropdown";
|
||||
import { formatNotificationCount } from "./helpers";
|
||||
import { useAgentNotifications } from "./useAgentNotifications";
|
||||
|
||||
export function AgentNotifications() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { activeExecutions, recentCompletions, recentFailures } =
|
||||
useAgentNotifications();
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className={`group relative rounded-full p-2 transition-colors hover:bg-white ${isOpen ? "bg-white" : ""}`}
|
||||
title="Agent Activity"
|
||||
>
|
||||
<Bell size={22} className="text-black" />
|
||||
|
||||
{activeExecutions.length > 0 && (
|
||||
<>
|
||||
{/* Running Agents Rotating Badge */}
|
||||
<div className="absolute right-[1px] top-[0.5px] flex h-5 w-5 items-center justify-center rounded-full bg-purple-600 text-[10px] font-medium text-white">
|
||||
{formatNotificationCount(activeExecutions.length)}
|
||||
<div className="absolute -inset-0.5 animate-spin rounded-full border-[3px] border-transparent border-r-purple-200 border-t-purple-200" />
|
||||
</div>
|
||||
{/* Running Agent Hover Hint */}
|
||||
<div className="absolute bottom-[-2.5rem] left-1/2 z-50 hidden -translate-x-1/2 transform whitespace-nowrap rounded-small bg-white px-4 py-2 shadow-md group-hover:block">
|
||||
<Text variant="body-medium">
|
||||
{activeExecutions.length} running agent
|
||||
{activeExecutions.length > 1 ? "s" : ""}
|
||||
</Text>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent className="w-80 p-0" align="center" sideOffset={8}>
|
||||
<NotificationDropdown
|
||||
activeExecutions={activeExecutions}
|
||||
recentCompletions={recentCompletions}
|
||||
recentFailures={recentFailures}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Bell } from "@phosphor-icons/react";
|
||||
import { AgentExecutionWithInfo } from "../helpers";
|
||||
import { NotificationItem } from "./NotificationItem";
|
||||
|
||||
interface NotificationDropdownProps {
|
||||
activeExecutions: AgentExecutionWithInfo[];
|
||||
recentCompletions: AgentExecutionWithInfo[];
|
||||
recentFailures: AgentExecutionWithInfo[];
|
||||
}
|
||||
|
||||
export function NotificationDropdown({
|
||||
activeExecutions,
|
||||
recentCompletions,
|
||||
recentFailures,
|
||||
}: NotificationDropdownProps) {
|
||||
// Combine and sort all executions - running/queued at top, then by most recent
|
||||
function getSortedExecutions() {
|
||||
const allExecutions = [
|
||||
...activeExecutions.map((e) => ({ ...e, type: "running" as const })),
|
||||
...recentCompletions.map((e) => ({ ...e, type: "completed" as const })),
|
||||
...recentFailures.map((e) => ({ ...e, type: "failed" as const })),
|
||||
];
|
||||
|
||||
return allExecutions.sort((a, b) => {
|
||||
// Running/queued always at top
|
||||
const aIsActive =
|
||||
a.status === AgentExecutionStatus.RUNNING ||
|
||||
a.status === AgentExecutionStatus.QUEUED;
|
||||
const bIsActive =
|
||||
b.status === AgentExecutionStatus.RUNNING ||
|
||||
b.status === AgentExecutionStatus.QUEUED;
|
||||
|
||||
if (aIsActive && !bIsActive) return -1;
|
||||
if (!aIsActive && bIsActive) return 1;
|
||||
|
||||
// Within same category, sort by most recent
|
||||
const aTime = aIsActive ? a.started_at : a.ended_at;
|
||||
const bTime = bIsActive ? b.started_at : b.ended_at;
|
||||
|
||||
if (!aTime || !bTime) return 0;
|
||||
return new Date(bTime).getTime() - new Date(aTime).getTime();
|
||||
});
|
||||
}
|
||||
|
||||
const sortedExecutions = getSortedExecutions();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 z-10 px-4 pb-1 pt-4">
|
||||
<Text variant="body-medium" className="font-semibold text-gray-900">
|
||||
Agent Activity
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<ScrollArea className="min-h-[10rem]">
|
||||
{sortedExecutions.length > 0 ? (
|
||||
<div className="p-2">
|
||||
{sortedExecutions.map((execution) => (
|
||||
<NotificationItem
|
||||
key={execution.id}
|
||||
execution={execution}
|
||||
type={execution.type}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
<Bell size={32} className="mx-auto mb-2 opacity-50" />
|
||||
<p>No recent activity</p>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import {
|
||||
CheckCircle,
|
||||
CircleNotchIcon,
|
||||
Clock,
|
||||
WarningOctagonIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { AgentExecutionWithInfo } from "../helpers";
|
||||
import {
|
||||
formatTimeAgo,
|
||||
getExecutionDuration,
|
||||
getStatusColorClass,
|
||||
} from "../helpers";
|
||||
|
||||
interface NotificationItemProps {
|
||||
execution: AgentExecutionWithInfo;
|
||||
type: "running" | "completed" | "failed";
|
||||
}
|
||||
|
||||
export function NotificationItem({ execution, type }: NotificationItemProps) {
|
||||
const router = useRouter();
|
||||
|
||||
function getStatusIcon() {
|
||||
switch (type) {
|
||||
case "running":
|
||||
return execution.status === AgentExecutionStatus.QUEUED ? (
|
||||
<Clock size={16} className="text-purple-500" />
|
||||
) : (
|
||||
<CircleNotchIcon
|
||||
size={16}
|
||||
className="animate-spin text-purple-500"
|
||||
weight="bold"
|
||||
/>
|
||||
);
|
||||
case "completed":
|
||||
return (
|
||||
<CheckCircle size={16} weight="fill" className="text-purple-500" />
|
||||
);
|
||||
case "failed":
|
||||
return <WarningOctagonIcon size={16} className="text-purple-500" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getTimeDisplay() {
|
||||
if (type === "running") {
|
||||
const timeAgo = formatTimeAgo(execution.started_at.toString());
|
||||
return `Started ${timeAgo}, ${getExecutionDuration(execution)} running`;
|
||||
}
|
||||
|
||||
if (execution.ended_at) {
|
||||
const timeAgo = formatTimeAgo(execution.ended_at.toString());
|
||||
return type === "completed"
|
||||
? `Completed ${timeAgo}`
|
||||
: `Failed ${timeAgo}`;
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="cursor-pointer border-b border-slate-50 px-2 py-3 transition-colors last:border-b-0 hover:bg-lightGrey"
|
||||
onClick={() => {
|
||||
const agentId = execution.library_agent_id || execution.graph_id;
|
||||
router.push(`/library/agents/${agentId}?executionId=${execution.id}`);
|
||||
}}
|
||||
role="button"
|
||||
>
|
||||
{/* Icon + Agent Name */}
|
||||
<div className="flex items-center space-x-3">
|
||||
{getStatusIcon()}
|
||||
<Text variant="body-medium" className="truncate text-gray-900">
|
||||
{execution.agent_name}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Agent Message - Indented */}
|
||||
<div className="ml-7">
|
||||
{execution.agent_description ? (
|
||||
<Text variant="body" className={`${getStatusColorClass(execution)}`}>
|
||||
{execution.agent_description}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
{/* Time - Indented */}
|
||||
<Text variant="small" className="pt-2 !text-zinc-500">
|
||||
{getTimeDisplay()}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface NotificationSectionProps {
|
||||
title: string;
|
||||
count: number;
|
||||
colorClass: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function NotificationSection({
|
||||
title,
|
||||
count,
|
||||
colorClass,
|
||||
children,
|
||||
}: NotificationSectionProps) {
|
||||
return (
|
||||
<div className="border-b border-gray-100 p-4 dark:border-gray-700">
|
||||
<h4 className={`mb-2 text-sm font-medium ${colorClass}`}>
|
||||
{title} ({count})
|
||||
</h4>
|
||||
<div className="space-y-2">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
|
||||
import { GraphExecutionMeta as GeneratedGraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta";
|
||||
import { MyAgent } from "@/app/api/__generated__/models/myAgent";
|
||||
import type { GraphExecution } from "@/lib/autogpt-server-api/types";
|
||||
|
||||
export function formatTimeAgo(dateStr: string): string {
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
|
||||
if (diffMins < 1) return "Just now";
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
return `${diffDays}d ago`;
|
||||
}
|
||||
|
||||
export function getStatusDisplayText(
|
||||
execution: GeneratedGraphExecutionMeta,
|
||||
): string {
|
||||
switch (execution.status) {
|
||||
case AgentExecutionStatus.QUEUED:
|
||||
return "Queued";
|
||||
case AgentExecutionStatus.RUNNING:
|
||||
return "Running";
|
||||
case AgentExecutionStatus.COMPLETED:
|
||||
return "Completed";
|
||||
case AgentExecutionStatus.FAILED:
|
||||
return "Failed";
|
||||
case AgentExecutionStatus.TERMINATED:
|
||||
return "Stopped";
|
||||
case AgentExecutionStatus.INCOMPLETE:
|
||||
return "Incomplete";
|
||||
default:
|
||||
return execution.status;
|
||||
}
|
||||
}
|
||||
|
||||
export function getStatusColorClass(
|
||||
execution: GeneratedGraphExecutionMeta,
|
||||
): string {
|
||||
switch (execution.status) {
|
||||
case AgentExecutionStatus.QUEUED:
|
||||
return "text-yellow-600";
|
||||
case AgentExecutionStatus.RUNNING:
|
||||
return "text-blue-600";
|
||||
case AgentExecutionStatus.COMPLETED:
|
||||
return "text-green-600";
|
||||
case AgentExecutionStatus.FAILED:
|
||||
case AgentExecutionStatus.TERMINATED:
|
||||
return "text-red-600";
|
||||
case AgentExecutionStatus.INCOMPLETE:
|
||||
return "text-gray-600";
|
||||
default:
|
||||
return "text-gray-600";
|
||||
}
|
||||
}
|
||||
|
||||
export function truncateGraphId(graphId: string, length: number = 8): string {
|
||||
return `${graphId.slice(0, length)}...`;
|
||||
}
|
||||
|
||||
export function getExecutionDuration(
|
||||
execution: GeneratedGraphExecutionMeta,
|
||||
): string {
|
||||
if (!execution.started_at) return "Unknown";
|
||||
|
||||
const start = new Date(execution.started_at);
|
||||
const end = execution.ended_at ? new Date(execution.ended_at) : new Date();
|
||||
const durationMs = end.getTime() - start.getTime();
|
||||
const durationSec = Math.floor(durationMs / 1000);
|
||||
|
||||
if (durationSec < 60) return `${durationSec}s`;
|
||||
const durationMin = Math.floor(durationSec / 60);
|
||||
if (durationMin < 60) return `${durationMin}m ${durationSec % 60}s`;
|
||||
const durationHr = Math.floor(durationMin / 60);
|
||||
return `${durationHr}h ${durationMin % 60}m`;
|
||||
}
|
||||
|
||||
export function shouldShowNotificationBadge(totalCount: number): boolean {
|
||||
return totalCount > 0;
|
||||
}
|
||||
|
||||
export function formatNotificationCount(count: number): string {
|
||||
if (count > 99) return "99+";
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
export interface AgentExecutionWithInfo extends GeneratedGraphExecutionMeta {
|
||||
agent_name: string;
|
||||
agent_description: string;
|
||||
library_agent_id?: string;
|
||||
}
|
||||
|
||||
export interface NotificationState {
|
||||
activeExecutions: AgentExecutionWithInfo[];
|
||||
recentCompletions: AgentExecutionWithInfo[];
|
||||
recentFailures: AgentExecutionWithInfo[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export function createAgentInfoMap(
|
||||
agents: MyAgent[],
|
||||
): Map<
|
||||
string,
|
||||
{ name: string; description: string; library_agent_id?: string }
|
||||
> {
|
||||
const agentMap = new Map<
|
||||
string,
|
||||
{ name: string; description: string; library_agent_id?: string }
|
||||
>();
|
||||
|
||||
agents.forEach((agent) => {
|
||||
agentMap.set(agent.agent_id, {
|
||||
name: agent.agent_name,
|
||||
description: agent.description,
|
||||
library_agent_id: undefined, // MyAgent doesn't have library_agent_id
|
||||
});
|
||||
});
|
||||
|
||||
return agentMap;
|
||||
}
|
||||
|
||||
export function convertLegacyExecutionToGenerated(
|
||||
execution: GraphExecution,
|
||||
): GeneratedGraphExecutionMeta {
|
||||
return {
|
||||
id: execution.id,
|
||||
user_id: execution.user_id,
|
||||
graph_id: execution.graph_id,
|
||||
graph_version: execution.graph_version,
|
||||
preset_id: execution.preset_id,
|
||||
status: execution.status as AgentExecutionStatus,
|
||||
started_at: execution.started_at.toISOString(),
|
||||
ended_at: execution.ended_at.toISOString(),
|
||||
stats: execution.stats || {
|
||||
cost: 0,
|
||||
duration: 0,
|
||||
duration_cpu_only: 0,
|
||||
node_exec_time: 0,
|
||||
node_exec_time_cpu_only: 0,
|
||||
node_exec_count: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function enrichExecutionWithAgentInfo(
|
||||
execution: GeneratedGraphExecutionMeta,
|
||||
agentInfoMap: Map<
|
||||
string,
|
||||
{ name: string; description: string; library_agent_id?: string }
|
||||
>,
|
||||
): AgentExecutionWithInfo {
|
||||
const agentInfo = agentInfoMap.get(execution.graph_id);
|
||||
return {
|
||||
...execution,
|
||||
agent_name: agentInfo?.name || `Graph ${execution.graph_id.slice(0, 8)}...`,
|
||||
agent_description: agentInfo?.description ?? "",
|
||||
library_agent_id: agentInfo?.library_agent_id,
|
||||
};
|
||||
}
|
||||
|
||||
export function isActiveExecution(
|
||||
execution: GeneratedGraphExecutionMeta,
|
||||
): boolean {
|
||||
const status = execution.status;
|
||||
return (
|
||||
status === AgentExecutionStatus.RUNNING ||
|
||||
status === AgentExecutionStatus.QUEUED
|
||||
);
|
||||
}
|
||||
|
||||
export function isRecentCompletion(
|
||||
execution: GeneratedGraphExecutionMeta,
|
||||
thirtyMinutesAgo: Date,
|
||||
): boolean {
|
||||
const status = execution.status;
|
||||
return (
|
||||
status === AgentExecutionStatus.COMPLETED &&
|
||||
!!execution.ended_at &&
|
||||
new Date(execution.ended_at) > thirtyMinutesAgo
|
||||
);
|
||||
}
|
||||
|
||||
export function isRecentFailure(
|
||||
execution: GeneratedGraphExecutionMeta,
|
||||
thirtyMinutesAgo: Date,
|
||||
): boolean {
|
||||
const status = execution.status;
|
||||
return (
|
||||
(status === AgentExecutionStatus.FAILED ||
|
||||
status === AgentExecutionStatus.TERMINATED) &&
|
||||
!!execution.ended_at &&
|
||||
new Date(execution.ended_at) > thirtyMinutesAgo
|
||||
);
|
||||
}
|
||||
|
||||
export function isRecentNotification(
|
||||
execution: AgentExecutionWithInfo,
|
||||
thirtyMinutesAgo: Date,
|
||||
): boolean {
|
||||
return execution.ended_at
|
||||
? new Date(execution.ended_at) > thirtyMinutesAgo
|
||||
: false;
|
||||
}
|
||||
|
||||
export function categorizeExecutions(
|
||||
executions: GeneratedGraphExecutionMeta[],
|
||||
agentInfoMap: Map<
|
||||
string,
|
||||
{ name: string; description: string; library_agent_id?: string }
|
||||
>,
|
||||
): NotificationState {
|
||||
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
|
||||
const enrichedExecutions = executions.map((execution) =>
|
||||
enrichExecutionWithAgentInfo(execution, agentInfoMap),
|
||||
);
|
||||
|
||||
const activeExecutions = enrichedExecutions
|
||||
.filter(isActiveExecution)
|
||||
.slice(0, 10);
|
||||
|
||||
const recentCompletions = enrichedExecutions
|
||||
.filter((execution) => isRecentCompletion(execution, twentyFourHoursAgo))
|
||||
.slice(0, 10);
|
||||
|
||||
const recentFailures = enrichedExecutions
|
||||
.filter((execution) => isRecentFailure(execution, twentyFourHoursAgo))
|
||||
.slice(0, 10);
|
||||
|
||||
return {
|
||||
activeExecutions,
|
||||
recentCompletions,
|
||||
recentFailures,
|
||||
totalCount:
|
||||
activeExecutions.length +
|
||||
recentCompletions.length +
|
||||
recentFailures.length,
|
||||
};
|
||||
}
|
||||
|
||||
export function removeExecutionFromAllCategories(
|
||||
state: NotificationState,
|
||||
executionId: string,
|
||||
): NotificationState {
|
||||
return {
|
||||
activeExecutions: state.activeExecutions.filter(
|
||||
(e) => e.id !== executionId,
|
||||
),
|
||||
recentCompletions: state.recentCompletions.filter(
|
||||
(e) => e.id !== executionId,
|
||||
),
|
||||
recentFailures: state.recentFailures.filter((e) => e.id !== executionId),
|
||||
totalCount: state.totalCount, // Will be recalculated later
|
||||
};
|
||||
}
|
||||
|
||||
export function addExecutionToCategory(
|
||||
state: NotificationState,
|
||||
execution: AgentExecutionWithInfo,
|
||||
): NotificationState {
|
||||
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
const newState = { ...state };
|
||||
|
||||
if (isActiveExecution(execution)) {
|
||||
newState.activeExecutions = [execution, ...newState.activeExecutions].slice(
|
||||
0,
|
||||
10,
|
||||
);
|
||||
} else if (isRecentCompletion(execution, twentyFourHoursAgo)) {
|
||||
newState.recentCompletions = [
|
||||
execution,
|
||||
...newState.recentCompletions,
|
||||
].slice(0, 10);
|
||||
} else if (isRecentFailure(execution, twentyFourHoursAgo)) {
|
||||
newState.recentFailures = [execution, ...newState.recentFailures].slice(
|
||||
0,
|
||||
10,
|
||||
);
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
export function cleanupOldNotifications(
|
||||
state: NotificationState,
|
||||
): NotificationState {
|
||||
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
|
||||
return {
|
||||
...state,
|
||||
recentCompletions: state.recentCompletions.filter((e) =>
|
||||
isRecentNotification(e, twentyFourHoursAgo),
|
||||
),
|
||||
recentFailures: state.recentFailures.filter((e) =>
|
||||
isRecentNotification(e, twentyFourHoursAgo),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function calculateTotalCount(
|
||||
state: NotificationState,
|
||||
): NotificationState {
|
||||
return {
|
||||
...state,
|
||||
totalCount:
|
||||
state.activeExecutions.length +
|
||||
state.recentCompletions.length +
|
||||
state.recentFailures.length,
|
||||
};
|
||||
}
|
||||
|
||||
export function handleExecutionUpdate(
|
||||
currentState: NotificationState,
|
||||
execution: GraphExecution,
|
||||
agentInfoMap: Map<
|
||||
string,
|
||||
{ name: string; description: string; library_agent_id?: string }
|
||||
>,
|
||||
): NotificationState {
|
||||
// Convert and enrich the execution
|
||||
const convertedExecution = convertLegacyExecutionToGenerated(execution);
|
||||
const enrichedExecution = enrichExecutionWithAgentInfo(
|
||||
convertedExecution,
|
||||
agentInfoMap,
|
||||
);
|
||||
|
||||
// Remove from all categories first
|
||||
let newState = removeExecutionFromAllCategories(currentState, execution.id);
|
||||
|
||||
// Add to appropriate category
|
||||
newState = addExecutionToCategory(newState, enrichedExecution);
|
||||
|
||||
// Clean up old notifications
|
||||
newState = cleanupOldNotifications(newState);
|
||||
|
||||
// Recalculate total count
|
||||
newState = calculateTotalCount(newState);
|
||||
|
||||
return newState;
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { useGetV1GetAllExecutions } from "@/app/api/__generated__/endpoints/graphs/graphs";
|
||||
import { useGetV2GetMyAgents } from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { useGetV2ListLibraryAgents } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import BackendAPI from "@/lib/autogpt-server-api/client";
|
||||
import type { GraphExecution } from "@/lib/autogpt-server-api/types";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
NotificationState,
|
||||
categorizeExecutions,
|
||||
createAgentInfoMap,
|
||||
handleExecutionUpdate,
|
||||
} from "./helpers";
|
||||
|
||||
type AgentInfoMap = Map<
|
||||
string,
|
||||
{ name: string; description: string; library_agent_id?: string }
|
||||
>;
|
||||
|
||||
export function useAgentNotifications() {
|
||||
const [api] = useState(() => new BackendAPI());
|
||||
const [notifications, setNotifications] = useState<NotificationState>({
|
||||
activeExecutions: [],
|
||||
recentCompletions: [],
|
||||
recentFailures: [],
|
||||
totalCount: 0,
|
||||
});
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [agentInfoMap, setAgentInfoMap] = useState<AgentInfoMap>(new Map());
|
||||
|
||||
// Get library agents using generated hook
|
||||
const {
|
||||
data: myAgentsResponse,
|
||||
isLoading: isAgentsLoading,
|
||||
error: agentsError,
|
||||
} = useGetV2GetMyAgents({
|
||||
query: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Get library agents data to map graph_id to library_agent_id
|
||||
const {
|
||||
data: libraryAgentsResponse,
|
||||
isLoading: isLibraryAgentsLoading,
|
||||
error: libraryAgentsError,
|
||||
} = useGetV2ListLibraryAgents(
|
||||
{},
|
||||
{
|
||||
query: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Get all executions using generated hook
|
||||
const {
|
||||
data: executionsResponse,
|
||||
isLoading: isExecutionsLoading,
|
||||
error: executionsError,
|
||||
} = useGetV1GetAllExecutions({
|
||||
query: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Update agent info map when both agent data sources change
|
||||
useEffect(() => {
|
||||
if (
|
||||
myAgentsResponse?.data?.agents &&
|
||||
libraryAgentsResponse?.data &&
|
||||
"agents" in libraryAgentsResponse.data
|
||||
) {
|
||||
const agentMap = createAgentInfoMap(myAgentsResponse.data.agents);
|
||||
|
||||
// Add library agent ID mapping
|
||||
libraryAgentsResponse.data.agents.forEach(
|
||||
(libraryAgent: LibraryAgent) => {
|
||||
const existingInfo = agentMap.get(libraryAgent.graph_id);
|
||||
if (existingInfo) {
|
||||
agentMap.set(libraryAgent.graph_id, {
|
||||
...existingInfo,
|
||||
library_agent_id: libraryAgent.id,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
setAgentInfoMap(agentMap);
|
||||
}
|
||||
}, [myAgentsResponse, libraryAgentsResponse]);
|
||||
|
||||
// Handle real-time execution updates
|
||||
const handleExecutionEvent = useCallback(
|
||||
(execution: GraphExecution) => {
|
||||
setNotifications((currentState) =>
|
||||
handleExecutionUpdate(currentState, execution, agentInfoMap),
|
||||
);
|
||||
},
|
||||
[agentInfoMap],
|
||||
);
|
||||
|
||||
// Process initial execution state when data loads
|
||||
useEffect(() => {
|
||||
if (
|
||||
executionsResponse?.data &&
|
||||
!isExecutionsLoading &&
|
||||
agentInfoMap.size > 0
|
||||
) {
|
||||
const newNotifications = categorizeExecutions(
|
||||
executionsResponse.data,
|
||||
agentInfoMap,
|
||||
);
|
||||
|
||||
setNotifications(newNotifications);
|
||||
}
|
||||
}, [executionsResponse, isExecutionsLoading, agentInfoMap]);
|
||||
|
||||
// Initialize WebSocket connection for real-time updates
|
||||
useEffect(() => {
|
||||
const connectHandler = api.onWebSocketConnect(() => {
|
||||
setIsConnected(true);
|
||||
|
||||
// Subscribe to graph executions for all user agents
|
||||
if (myAgentsResponse?.data?.agents) {
|
||||
myAgentsResponse.data.agents.forEach((agent) => {
|
||||
api
|
||||
.subscribeToGraphExecutions(agent.agent_id as any)
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
`[AgentNotifications] Failed to subscribe to graph ${agent.agent_id}:`,
|
||||
error,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const disconnectHandler = api.onWebSocketDisconnect(() => {
|
||||
setIsConnected(false);
|
||||
});
|
||||
|
||||
const messageHandler = api.onWebSocketMessage(
|
||||
"graph_execution_event",
|
||||
handleExecutionEvent,
|
||||
);
|
||||
|
||||
api.connectWebSocket();
|
||||
|
||||
return () => {
|
||||
connectHandler();
|
||||
disconnectHandler();
|
||||
messageHandler();
|
||||
api.disconnectWebSocket();
|
||||
};
|
||||
}, [api, handleExecutionEvent, myAgentsResponse]);
|
||||
|
||||
return {
|
||||
...notifications,
|
||||
isConnected,
|
||||
isLoading: isAgentsLoading || isExecutionsLoading || isLibraryAgentsLoading,
|
||||
error: agentsError || executionsError || libraryAgentsError,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { SignInIcon } from "@phosphor-icons/react/dist/ssr";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export function LoginButton() {
|
||||
const pathname = usePathname();
|
||||
const isLoginPage = pathname.includes("/login");
|
||||
|
||||
if (isLoginPage) return null;
|
||||
|
||||
return (
|
||||
<Button
|
||||
as="NextLink"
|
||||
href="/login"
|
||||
size="small"
|
||||
className="flex items-center justify-end space-x-2"
|
||||
leftIcon={<SignInIcon className="h-5 w-5" />}
|
||||
variant="secondary"
|
||||
>
|
||||
Log In
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,45 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import Link from "next/link";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
PopoverPortal,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
IconType,
|
||||
IconMenu,
|
||||
IconChevronUp,
|
||||
IconEdit,
|
||||
IconLayoutDashboard,
|
||||
IconUploadCloud,
|
||||
IconSettings,
|
||||
IconLogOut,
|
||||
IconMarketplace,
|
||||
IconLibrary,
|
||||
IconBuilder,
|
||||
} from "../ui/icons";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { usePathname } from "next/navigation";
|
||||
import * as React from "react";
|
||||
import { IconChevronUp, IconMenu } from "../../../../ui/icons";
|
||||
import { MenuItemGroup } from "../../helpers";
|
||||
import { MobileNavbarMenuItem } from "./components/MobileNavbarMenuItem";
|
||||
|
||||
interface MobileNavBarProps {
|
||||
userName?: string;
|
||||
userEmail?: string;
|
||||
avatarSrc?: string;
|
||||
menuItemGroups: {
|
||||
groupName?: string;
|
||||
items: {
|
||||
icon: IconType;
|
||||
text: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
}[];
|
||||
}[];
|
||||
menuItemGroups: MenuItemGroup[];
|
||||
}
|
||||
|
||||
const Overlay = React.forwardRef<HTMLDivElement, { children: React.ReactNode }>(
|
||||
@@ -49,76 +30,15 @@ const Overlay = React.forwardRef<HTMLDivElement, { children: React.ReactNode }>(
|
||||
</div>
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.displayName = "Overlay";
|
||||
|
||||
const PopoutMenuItem: React.FC<{
|
||||
icon: IconType;
|
||||
isActive: boolean;
|
||||
text: React.ReactNode;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
}> = ({ icon, isActive, text, href, onClick }) => {
|
||||
const getIcon = (iconType: IconType) => {
|
||||
const iconClass = "w-6 h-6 relative";
|
||||
switch (iconType) {
|
||||
case IconType.Marketplace:
|
||||
return <IconMarketplace className={iconClass} />;
|
||||
case IconType.Library:
|
||||
return <IconLibrary className={iconClass} />;
|
||||
case IconType.Builder:
|
||||
return <IconBuilder className={iconClass} />;
|
||||
case IconType.Edit:
|
||||
return <IconEdit className={iconClass} />;
|
||||
case IconType.LayoutDashboard:
|
||||
return <IconLayoutDashboard className={iconClass} />;
|
||||
case IconType.UploadCloud:
|
||||
return <IconUploadCloud className={iconClass} />;
|
||||
case IconType.Settings:
|
||||
return <IconSettings className={iconClass} />;
|
||||
case IconType.LogOut:
|
||||
return <IconLogOut className={iconClass} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const content = (
|
||||
<div className="inline-flex w-full items-center justify-start gap-4 hover:rounded hover:bg-[#e0e0e0] dark:hover:bg-[#3a3a3a]">
|
||||
{getIcon(icon)}
|
||||
<div className="relative">
|
||||
<div
|
||||
className={`font-sans text-base font-normal leading-7 text-[#474747] dark:text-[#cfcfcf] ${isActive ? "font-semibold text-[#272727] dark:text-[#ffffff]" : "text-[#474747] dark:text-[#cfcfcf]"}`}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
{isActive && (
|
||||
<div className="absolute bottom-[-4px] left-0 h-[2px] w-full bg-[#272727] dark:bg-[#ffffff]"></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (onClick)
|
||||
return (
|
||||
<div className="w-full" onClick={onClick}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
if (href)
|
||||
return (
|
||||
<Link href={href} className="w-full">
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
return content;
|
||||
};
|
||||
|
||||
export const MobileNavBar: React.FC<MobileNavBarProps> = ({
|
||||
export function MobileNavBar({
|
||||
userName,
|
||||
userEmail,
|
||||
avatarSrc,
|
||||
menuItemGroups,
|
||||
}) => {
|
||||
}: MobileNavBarProps) {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const pathname = usePathname();
|
||||
const parts = pathname.split("/");
|
||||
@@ -173,7 +93,7 @@ export const MobileNavBar: React.FC<MobileNavBarProps> = ({
|
||||
{menuItemGroups.map((group, groupIndex) => (
|
||||
<React.Fragment key={groupIndex}>
|
||||
{group.items.map((item, itemIndex) => (
|
||||
<PopoutMenuItem
|
||||
<MobileNavbarMenuItem
|
||||
key={itemIndex}
|
||||
icon={item.icon}
|
||||
isActive={item.href === activeLink}
|
||||
@@ -194,4 +114,4 @@ export const MobileNavBar: React.FC<MobileNavBarProps> = ({
|
||||
</AnimatePresence>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { IconType } from "@/components/ui/icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import { getAccountMenuOptionIcon } from "../../../helpers";
|
||||
|
||||
interface Props {
|
||||
icon: IconType;
|
||||
isActive: boolean;
|
||||
text: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export function MobileNavbarMenuItem({
|
||||
icon,
|
||||
isActive,
|
||||
text,
|
||||
href,
|
||||
onClick,
|
||||
}: Props) {
|
||||
const content = (
|
||||
<div className="inline-flex w-full items-center justify-start gap-4 hover:rounded hover:bg-[#e0e0e0] dark:hover:bg-[#3a3a3a]">
|
||||
{getAccountMenuOptionIcon(icon)}
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
"font-sans text-base font-normal leading-7",
|
||||
isActive
|
||||
? "font-semibold text-[#272727] dark:text-[#ffffff]"
|
||||
: "text-[#474747] dark:text-[#cfcfcf]",
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
{isActive && (
|
||||
<div className="absolute bottom-[-4px] left-0 h-[2px] w-full bg-[#272727] dark:bg-[#ffffff]"></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (onClick)
|
||||
return (
|
||||
<div className="w-full" onClick={onClick}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
if (href)
|
||||
return (
|
||||
<Link href={href} className="w-full">
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
return content;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import { IconLaptop } from "@/components/ui/icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
CubeIcon,
|
||||
HouseIcon,
|
||||
StorefrontIcon,
|
||||
} from "@phosphor-icons/react/dist/ssr";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Text } from "../../../atoms/Text/Text";
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export function NavbarLink({ name, href }: Props) {
|
||||
const pathname = usePathname();
|
||||
const isActive = pathname.includes(href);
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
data-testid={`navbar-link-${name.toLowerCase()}`}
|
||||
className="font-poppins text-[20px] leading-[28px]"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-start gap-1 p-2",
|
||||
isActive &&
|
||||
"rounded-small bg-neutral-800 p-2 transition-all duration-300 dark:bg-neutral-200",
|
||||
)}
|
||||
>
|
||||
{href === "/marketplace" && (
|
||||
<StorefrontIcon
|
||||
className={cn("h-6 w-6", isActive && "text-white dark:text-black")}
|
||||
/>
|
||||
)}
|
||||
{href === "/build" && (
|
||||
<CubeIcon
|
||||
className={cn("h-6 w-6", isActive && "text-white dark:text-black")}
|
||||
/>
|
||||
)}
|
||||
{href === "/monitor" && (
|
||||
<IconLaptop
|
||||
className={cn("h-6 w-6", isActive && "text-white dark:text-black")}
|
||||
/>
|
||||
)}
|
||||
{href === "/library" && (
|
||||
<HouseIcon
|
||||
className={cn("h-6 w-6", isActive && "text-white dark:text-black")}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
variant="body-medium"
|
||||
className={cn(
|
||||
"hidden lg:block",
|
||||
isActive ? "!text-white" : "!text-black",
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { IconAutoGPTLogo } from "@/components/ui/icons";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export function NavbarLoading() {
|
||||
return (
|
||||
<nav className="sticky top-0 z-40 hidden h-16 items-center rounded-bl-2xl rounded-br-2xl border border-white/50 bg-white/5 p-3 backdrop-blur-[26px] md:inline-flex">
|
||||
<div className="flex flex-1 items-center gap-6">
|
||||
<Skeleton className="h-4 w-20 bg-white/20" />
|
||||
<Skeleton className="h-4 w-16 bg-white/20" />
|
||||
<Skeleton className="h-4 w-12 bg-white/20" />
|
||||
</div>
|
||||
<div className="absolute left-1/2 top-1/2 h-10 w-[88.87px] -translate-x-1/2 -translate-y-1/2">
|
||||
<IconAutoGPTLogo className="h-full w-full" />
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-end gap-4">
|
||||
<Skeleton className="h-8 w-8 rounded-full bg-white/20" />
|
||||
<Skeleton className="h-8 w-8 rounded-full bg-white/20" />
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
IconBuilder,
|
||||
IconEdit,
|
||||
IconLayoutDashboard,
|
||||
IconLibrary,
|
||||
IconLogOut,
|
||||
IconMarketplace,
|
||||
IconRefresh,
|
||||
IconSettings,
|
||||
IconType,
|
||||
IconUploadCloud,
|
||||
} from "@/components/ui/icons";
|
||||
|
||||
type Link = {
|
||||
name: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export const loggedInLinks: Link[] = [
|
||||
{
|
||||
name: "Marketplace",
|
||||
href: "/marketplace",
|
||||
},
|
||||
{
|
||||
name: "Library",
|
||||
href: "/library",
|
||||
},
|
||||
{
|
||||
name: "Build",
|
||||
href: "/build",
|
||||
},
|
||||
];
|
||||
|
||||
export const loggedOutLinks: Link[] = [
|
||||
{
|
||||
name: "Marketplace",
|
||||
href: "/marketplace",
|
||||
},
|
||||
];
|
||||
|
||||
export type MenuItemGroup = {
|
||||
groupName?: string;
|
||||
items: {
|
||||
icon: IconType;
|
||||
text: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
}[];
|
||||
};
|
||||
|
||||
export const accountMenuItems: MenuItemGroup[] = [
|
||||
{
|
||||
items: [
|
||||
{
|
||||
icon: IconType.Edit,
|
||||
text: "Edit profile",
|
||||
href: "/profile",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{
|
||||
icon: IconType.LayoutDashboard,
|
||||
text: "Creator Dashboard",
|
||||
href: "/profile/dashboard",
|
||||
},
|
||||
{
|
||||
icon: IconType.UploadCloud,
|
||||
text: "Publish an agent",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{
|
||||
icon: IconType.Settings,
|
||||
text: "Settings",
|
||||
href: "/profile/settings",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{
|
||||
icon: IconType.LogOut,
|
||||
text: "Log out",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function getAccountMenuOptionIcon(icon: IconType) {
|
||||
const iconClass = "w-6 h-6";
|
||||
switch (icon) {
|
||||
case IconType.LayoutDashboard:
|
||||
return <IconLayoutDashboard className={iconClass} />;
|
||||
case IconType.UploadCloud:
|
||||
return <IconUploadCloud className={iconClass} />;
|
||||
case IconType.Edit:
|
||||
return <IconEdit className={iconClass} />;
|
||||
case IconType.Settings:
|
||||
return <IconSettings className={iconClass} />;
|
||||
case IconType.LogOut:
|
||||
return <IconLogOut className={iconClass} />;
|
||||
case IconType.Marketplace:
|
||||
return <IconMarketplace className={iconClass} />;
|
||||
case IconType.Library:
|
||||
return <IconLibrary className={iconClass} />;
|
||||
case IconType.Builder:
|
||||
return <IconBuilder className={iconClass} />;
|
||||
default:
|
||||
return <IconRefresh className={iconClass} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
|
||||
export function useNavbar() {
|
||||
const { isLoggedIn, isUserLoading } = useSupabase();
|
||||
|
||||
const {
|
||||
data: profileResponse,
|
||||
isLoading: isProfileLoading,
|
||||
error: profileError,
|
||||
} = useGetV2GetUserProfile({
|
||||
query: {
|
||||
enabled: isLoggedIn === true,
|
||||
},
|
||||
});
|
||||
|
||||
const profile = profileResponse?.data || null;
|
||||
const isLoading = isUserLoading || (isLoggedIn && isProfileLoading);
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
profile,
|
||||
isLoading,
|
||||
profileError,
|
||||
};
|
||||
}
|
||||
@@ -71,6 +71,30 @@ export const colors = {
|
||||
800: "#0c5a29",
|
||||
900: "#09441f",
|
||||
},
|
||||
purple: {
|
||||
50: "#f1ebfe",
|
||||
100: "#d5c0fc",
|
||||
200: "#c0a1fa",
|
||||
300: "#a476f8",
|
||||
400: "#925cf7",
|
||||
500: "#7733f5",
|
||||
600: "#6c2edf",
|
||||
700: "#5424ae",
|
||||
800: "#411c87",
|
||||
900: "#321567",
|
||||
},
|
||||
pink: {
|
||||
50: "#fdedf5",
|
||||
100: "#f9c6df",
|
||||
200: "#f6abd0",
|
||||
300: "#f284bb",
|
||||
400: "#f06dad",
|
||||
500: "#ec4899",
|
||||
600: "#d7428b",
|
||||
700: "#a8336d",
|
||||
800: "#822854",
|
||||
900: "#631e40",
|
||||
},
|
||||
|
||||
// Special semantic colors
|
||||
white: "#fefefe",
|
||||
|
||||
@@ -14,54 +14,54 @@ const meta: Meta = {
|
||||
export default meta;
|
||||
|
||||
// Border radius scale data based on Figma design tokens
|
||||
// Custom naming convention: xs, s, m, l, xl, 2xl, full
|
||||
// Custom naming convention: xsmall, small, medium, large, xlarge, 2xlarge, full
|
||||
const borderRadiusScale = [
|
||||
{
|
||||
name: "xs",
|
||||
name: "xsmall",
|
||||
value: "0.25rem",
|
||||
rem: "0.25rem",
|
||||
px: "4px",
|
||||
class: "rounded-xs",
|
||||
class: "rounded-xsmall",
|
||||
description: "Extra small - for subtle rounding",
|
||||
},
|
||||
{
|
||||
name: "s",
|
||||
name: "small",
|
||||
value: "0.5rem",
|
||||
rem: "0.5rem",
|
||||
px: "8px",
|
||||
class: "rounded-s",
|
||||
class: "rounded-small",
|
||||
description: "Small - for cards and containers",
|
||||
},
|
||||
{
|
||||
name: "m",
|
||||
name: "medium",
|
||||
value: "0.75rem",
|
||||
rem: "0.75rem",
|
||||
px: "12px",
|
||||
class: "rounded-m",
|
||||
class: "rounded-medium",
|
||||
description: "Medium - for buttons and inputs",
|
||||
},
|
||||
{
|
||||
name: "l",
|
||||
name: "large",
|
||||
value: "1rem",
|
||||
rem: "1rem",
|
||||
px: "16px",
|
||||
class: "rounded-l",
|
||||
class: "rounded-large",
|
||||
description: "Large - for panels and modals",
|
||||
},
|
||||
{
|
||||
name: "xl",
|
||||
name: "xlarge",
|
||||
value: "1.25rem",
|
||||
rem: "1.25rem",
|
||||
px: "20px",
|
||||
class: "rounded-xl",
|
||||
class: "rounded-xlarge",
|
||||
description: "Extra large - for hero sections",
|
||||
},
|
||||
{
|
||||
name: "2xl",
|
||||
name: "2xlarge",
|
||||
value: "1.5rem",
|
||||
rem: "1.5rem",
|
||||
px: "24px",
|
||||
class: "rounded-2xl",
|
||||
class: "rounded-2xlarge",
|
||||
description: "2X large - for major containers",
|
||||
},
|
||||
{
|
||||
@@ -84,10 +84,11 @@ export function AllVariants() {
|
||||
Border Radius
|
||||
</Text>
|
||||
<Text variant="large" className="text-zinc-600">
|
||||
Our border radius system uses a simplified naming convention (xs, s,
|
||||
m, l, xl, 2xl, full) based on our Figma design tokens. This creates
|
||||
visual hierarchy and maintains design consistency across all
|
||||
components.
|
||||
Our border radius system uses a descriptive naming convention
|
||||
(xsmall, small, medium, large, xlarge, 2xlarge, full) based on our
|
||||
Figma design tokens. This creates visual hierarchy and maintains
|
||||
design consistency across all components while avoiding conflicts
|
||||
with Tailwind's built-in classes.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
@@ -137,9 +138,10 @@ export function AllVariants() {
|
||||
</div>
|
||||
<Text variant="body" className="mb-4 text-zinc-600">
|
||||
We use a custom border radius system based on our Figma design
|
||||
tokens, with simplified naming (xs, s, m, l, xl, 2xl, full) that
|
||||
provides consistent radius values optimized for our design
|
||||
system.
|
||||
tokens, with descriptive naming (xsmall, small, medium, large,
|
||||
xlarge, 2xlarge, full) that provides consistent radius values
|
||||
optimized for our design system while avoiding conflicts with
|
||||
Tailwind's built-in classes.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
@@ -188,7 +190,8 @@ export function AllVariants() {
|
||||
<Text variant="body" className="mb-6 text-zinc-600">
|
||||
All border radius values from our Figma design tokens. Each value
|
||||
can be applied to all corners or specific corners/sides using our
|
||||
simplified naming convention.
|
||||
descriptive naming convention (xsmall, small, medium, large, xlarge,
|
||||
2xlarge, full).
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
@@ -232,30 +235,30 @@ export function AllVariants() {
|
||||
|
||||
<StoryCode
|
||||
code={`// Border radius examples - Design System Tokens
|
||||
<div className="rounded-xs">Extra small rounding (4px)</div>
|
||||
<div className="rounded-s">Small rounding (8px)</div>
|
||||
<div className="rounded-m">Medium rounding (12px)</div>
|
||||
<div className="rounded-l">Large rounding (16px)</div>
|
||||
<div className="rounded-xl">Extra large rounding (20px)</div>
|
||||
<div className="rounded-2xl">2X large rounding (24px)</div>
|
||||
<div className="rounded-xsmall">Extra small rounding (4px)</div>
|
||||
<div className="rounded-small">Small rounding (8px)</div>
|
||||
<div className="rounded-medium">Medium rounding (12px)</div>
|
||||
<div className="rounded-large">Large rounding (16px)</div>
|
||||
<div className="rounded-xlarge">Extra large rounding (20px)</div>
|
||||
<div className="rounded-2xlarge">2X large rounding (24px)</div>
|
||||
<div className="rounded-full">Pill buttons (circular)</div>
|
||||
|
||||
// Directional rounding (works with all sizes)
|
||||
<div className="rounded-t-m">Top corners only</div>
|
||||
<div className="rounded-r-m">Right corners only</div>
|
||||
<div className="rounded-b-m">Bottom corners only</div>
|
||||
<div className="rounded-l-m">Left corners only</div>
|
||||
<div className="rounded-t-medium">Top corners only</div>
|
||||
<div className="rounded-r-medium">Right corners only</div>
|
||||
<div className="rounded-b-medium">Bottom corners only</div>
|
||||
<div className="rounded-l-medium">Left corners only</div>
|
||||
|
||||
// Individual corners
|
||||
<div className="rounded-tl-m">Top-left corner</div>
|
||||
<div className="rounded-tr-m">Top-right corner</div>
|
||||
<div className="rounded-bl-m">Bottom-left corner</div>
|
||||
<div className="rounded-br-m">Bottom-right corner</div>
|
||||
<div className="rounded-tl-medium">Top-left corner</div>
|
||||
<div className="rounded-tr-medium">Top-right corner</div>
|
||||
<div className="rounded-bl-medium">Bottom-left corner</div>
|
||||
<div className="rounded-br-medium">Bottom-right corner</div>
|
||||
|
||||
// Usage recommendations
|
||||
<button className="rounded-full">Pill Button</button>
|
||||
<div className="rounded-m">Card Container</div>
|
||||
<input className="rounded-s">Input Field</input>`}
|
||||
<div className="rounded-medium">Card Container</div>
|
||||
<input className="rounded-small">Input Field</input>`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,6 +36,8 @@ const colorCategories = Object.entries(colors)
|
||||
orange: "Warnings, notifications, and secondary call-to-actions",
|
||||
yellow: "Highlights, cautions, and attention-grabbing elements",
|
||||
green: "Success states, confirmations, and positive actions",
|
||||
purple: "Brand accents, premium features, and creative elements",
|
||||
pink: "Highlights, special promotions, and playful interactions",
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -312,6 +314,8 @@ export function AllVariants() {
|
||||
<div className="bg-green-50 border-green-200 text-green-800">Success</div>
|
||||
<div className="bg-red-50 border-red-200 text-red-800">Error</div>
|
||||
<div className="bg-yellow-50 border-yellow-200 text-yellow-800">Warning</div>
|
||||
<div className="bg-purple-50 border-purple-200 text-purple-800">Premium</div>
|
||||
<div className="bg-pink-50 border-pink-200 text-pink-800">Special</div>
|
||||
|
||||
// ❌ INCORRECT - Don't use these
|
||||
<div className="bg-blue-500 text-purple-600">❌ Not approved</div>
|
||||
|
||||
@@ -169,8 +169,6 @@ export function AllVariants() {
|
||||
code={`<Text variant="small">Small</Text>
|
||||
<Text variant="small-medium">Small Medium</Text>`}
|
||||
/>
|
||||
<Text variant="subtle">Subtle</Text>
|
||||
<StoryCode code={`<Text variant="subtle">Subtle</Text>`} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter, usePathname, useSearchParams } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export function PaginationControls({
|
||||
function PaginationControlsContent({
|
||||
currentPage,
|
||||
totalPages,
|
||||
pathParam = "page",
|
||||
@@ -48,3 +49,27 @@ export function PaginationControls({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PaginationControls({
|
||||
currentPage,
|
||||
totalPages,
|
||||
pathParam = "page",
|
||||
}: {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
pathParam?: string;
|
||||
}) {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex items-center justify-center">Loading...</div>
|
||||
}
|
||||
>
|
||||
<PaginationControlsContent
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
pathParam={pathParam}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -23,7 +23,7 @@ const PopoverContent = React.forwardRef<
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border border-neutral-200 bg-white p-4 text-neutral-950 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
|
||||
"rounded-medium z-50 w-72 border border-neutral-200 bg-white p-4 text-neutral-950 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -34,8 +34,8 @@ PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
PopoverAnchor,
|
||||
PopoverContent,
|
||||
PopoverPortal,
|
||||
PopoverTrigger,
|
||||
};
|
||||
|
||||
@@ -11,12 +11,12 @@ const ScrollArea = React.forwardRef<
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
className="h-full w-full rounded-[inherit]"
|
||||
style={{ overflow: "scroll" }}
|
||||
style={{ overflowX: "hidden", overflowY: "scroll" }}
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
|
||||
@@ -73,56 +73,56 @@ const config = {
|
||||
},
|
||||
spacing: {
|
||||
// Tailwind spacing + custom sizes
|
||||
0: "0rem",
|
||||
0.5: "0.125rem",
|
||||
1: "0.25rem",
|
||||
1.5: "0.375rem",
|
||||
2: "0.5rem",
|
||||
2.5: "0.625rem",
|
||||
3: "0.75rem",
|
||||
3.5: "0.875rem",
|
||||
4: "1rem",
|
||||
5: "1.25rem",
|
||||
6: "1.5rem",
|
||||
7: "1.75rem",
|
||||
7.5: "1.875rem",
|
||||
8: "2rem",
|
||||
8.5: "2.125rem",
|
||||
9: "2.25rem",
|
||||
10: "2.5rem",
|
||||
11: "2.75rem",
|
||||
12: "3rem",
|
||||
14: "3.5rem",
|
||||
16: "4rem",
|
||||
18: "4.5rem",
|
||||
20: "5rem",
|
||||
24: "6rem",
|
||||
28: "7rem",
|
||||
32: "8rem",
|
||||
36: "9rem",
|
||||
40: "10rem",
|
||||
44: "11rem",
|
||||
48: "12rem",
|
||||
52: "13rem",
|
||||
56: "14rem",
|
||||
60: "15rem",
|
||||
64: "16rem",
|
||||
68: "17rem",
|
||||
70: "17.5rem",
|
||||
71: "17.75rem",
|
||||
72: "18rem",
|
||||
76: "19rem",
|
||||
80: "20rem",
|
||||
96: "24rem",
|
||||
0: "0rem", // 0px
|
||||
0.5: "0.125rem", // 2px
|
||||
1: "0.25rem", // 4px
|
||||
1.5: "0.375rem", // 6px
|
||||
2: "0.5rem", // 8px
|
||||
2.5: "0.625rem", // 10px
|
||||
3: "0.75rem", // 12px
|
||||
3.5: "0.875rem", // 14px
|
||||
4: "1rem", // 16px
|
||||
5: "1.25rem", // 20px
|
||||
6: "1.5rem", // 24px
|
||||
7: "1.75rem", // 28px
|
||||
7.5: "1.875rem", // 30px
|
||||
8: "2rem", // 32px
|
||||
8.5: "2.125rem", // 34px
|
||||
9: "2.25rem", // 36px
|
||||
10: "2.5rem", // 40px
|
||||
11: "2.75rem", // 44px
|
||||
12: "3rem", // 48px
|
||||
14: "3.5rem", // 56px
|
||||
16: "4rem", // 64px
|
||||
18: "4.5rem", // 72px
|
||||
20: "5rem", // 80px
|
||||
24: "6rem", // 96px
|
||||
28: "7rem", // 112px
|
||||
32: "8rem", // 128px
|
||||
36: "9rem", // 144px
|
||||
40: "10rem", // 160px
|
||||
44: "11rem", // 176px
|
||||
48: "12rem", // 192px
|
||||
52: "13rem", // 208px
|
||||
56: "14rem", // 224px
|
||||
60: "15rem", // 240px
|
||||
64: "16rem", // 256px
|
||||
68: "17rem", // 272px
|
||||
70: "17.5rem", // 280px
|
||||
71: "17.75rem", // 284px
|
||||
72: "18rem", // 288px
|
||||
76: "19rem", // 304px
|
||||
80: "20rem", // 320px
|
||||
96: "24rem", // 384px
|
||||
},
|
||||
borderRadius: {
|
||||
// Design system border radius tokens from Figma
|
||||
xs: "0.25rem", // 4px
|
||||
s: "0.5rem", // 8px
|
||||
m: "0.75rem", // 12px
|
||||
l: "1rem", // 16px
|
||||
xl: "1.25rem", // 20px
|
||||
"2xl": "1.5rem", // 24px
|
||||
xsmall: "0.25rem", // 4px
|
||||
small: "0.5rem", // 8px
|
||||
medium: "0.75rem", // 12px
|
||||
large: "1rem", // 16px
|
||||
xlarge: "1.25rem", // 20px
|
||||
"2xlarge": "1.5rem", // 24px
|
||||
full: "9999px", // For pill buttons
|
||||
|
||||
// Legacy values - kept for backward compatibility
|
||||
|
||||
Reference in New Issue
Block a user