refactor(frontend): Update Supabase and backend API management (#9036)

Currently there are random issues (logout, auth desync) and
inconveniences with how Supabase and backend API works.
Resolves:
- https://github.com/Significant-Gravitas/AutoGPT/issues/9006
- https://github.com/Significant-Gravitas/AutoGPT/issues/8912

### Changes 🏗️

This PR streamlines how the Supabase and backend API is used to fix
current errors with auth, remove unnecessary code and make it easier to
use Supabase and backend API.

- Add `getServerSupabase` for server side that returns `SupabaseClient`.
- Add `Spinner` component that is used for loading animation.
- Remove redundant `useUser`, user is fetched in `useSupabase` already.
- Replace most Supabase `create*Client` to `getSupabaseServer` and
`useSupabase`.
- Remove redundant `AutoGPTServerAPI` class and rename
`BaseAutoGPTServerAPI` to `BackendAPI` and use it instead.
- Remove `SupabaseProvider` context; supabase caches internally what's
possible already.
- Move `useSupabase` hook to its own file and update it.

### Helpful table
| Next.js usage | Server | Client |
|---|---|---|
| API | `new BackendAPI();` | `new BackendAPI();`* or `useBackendAPI()`
|
| Supabase | `getServerSupabase();` | `useSupabase();` |
| user, user.role | `getServerUser();`** | `useSupabase();` |

\* `BackendAPI` automatically chooses correct Supabase client, so while
it's recommended to use `useBackendAPI()`, it's ok to use `new
BackendAPI();` in client components and even memoize it: `useMemo(() =>
new BackendAPI(), [])`.

** The reason user isn't returned in `getServerSupabase` is because it
forces async fetch but creating supabase doesn't, so it'd force
`getServerSupabase` to be async or return `{ supabase: SupabaseClient,
user: Promise<User> | null }`. For the same reason `useSupabase`
provides access to `supabase` immediately but `user` *may* be loading,
so there's `isUserLoading` provided as well.

### Checklist 📋

#### For code changes:
- [ ] I have clearly listed my changes in the PR description
- [ ] I have made a test plan
- [ ] I have tested my changes according to the test plan:
  <!-- Put your test plan here: -->
  - [ ] ...

<details>
  <summary>Example test plan</summary>
  
  - [ ] Create from scratch and execute an agent with at least 3 blocks
- [ ] Import an agent from file upload, and confirm it executes
correctly
  - [ ] Upload agent to marketplace
- [ ] Import an agent from marketplace and confirm it executes correctly
  - [ ] Edit an agent from monitor, and confirm it executes correctly
</details>

#### For configuration changes:
- [ ] `.env.example` is updated or already compatible with my changes
- [ ] `docker-compose.yml` is updated or already compatible with my
changes
- [ ] I have included a list of my configuration changes in the PR
description (under **Changes**)

<details>
  <summary>Examples of configuration changes</summary>

  - Changing ports
  - Adding new services that need to communicate with each other
  - Secrets or environment variable changes
  - New or infrastructure changes such as databases
</details>

---------

Co-authored-by: Aarushi <50577581+aarushik93@users.noreply.github.com>
Co-authored-by: Nicholas Tindle <nicholas.tindle@agpt.co>
This commit is contained in:
Krzysztof Czerwinski 2024-12-18 09:55:23 +00:00 committed by GitHub
parent 95bd268de8
commit 6ec2bacb72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 889 additions and 1160 deletions

View File

@ -8,7 +8,7 @@
* - Please do NOT serve this file on production.
*/
const PACKAGE_VERSION = '2.6.8'
const PACKAGE_VERSION = '2.7.0'
const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()

View File

@ -1,5 +1,5 @@
import getServerSupabase from "@/lib/supabase/getServerSupabase";
import { NextResponse } from "next/server";
import { createServerClient } from "@/lib/supabase/server";
// Handle the callback to complete the user session login
export async function GET(request: Request) {
@ -9,7 +9,7 @@ export async function GET(request: Request) {
const next = searchParams.get("next") ?? "/";
if (code) {
const supabase = createServerClient();
const supabase = getServerSupabase();
if (!supabase) {
return NextResponse.redirect(`${origin}/error`);

View File

@ -2,7 +2,7 @@ import { type EmailOtpType } from "@supabase/supabase-js";
import { type NextRequest } from "next/server";
import { redirect } from "next/navigation";
import { createServerClient } from "@/lib/supabase/server";
import getServerSupabase from "@/lib/supabase/getServerSupabase";
// Email confirmation route
export async function GET(request: NextRequest) {
@ -12,7 +12,7 @@ export async function GET(request: NextRequest) {
const next = searchParams.get("next") ?? "/";
if (token_hash && type) {
const supabase = createServerClient();
const supabase = getServerSupabase();
if (!supabase) {
redirect("/error");

View File

@ -10,7 +10,6 @@ import TallyPopupSimple from "@/components/TallyPopup";
import { GoogleAnalytics } from "@next/third-parties/google";
import { Toaster } from "@/components/ui/toaster";
import { IconType } from "@/components/ui/icons";
import { createServerClient } from "@/lib/supabase/server";
const inter = Inter({ subsets: ["latin"] });
@ -24,17 +23,10 @@ export default async function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
const supabase = createServerClient();
const {
data: { user },
} = await supabase.auth.getUser();
return (
<html lang="en">
<body className={cn("antialiased transition-colors", inter.className)}>
<Providers
initialUser={user}
attribute="class"
defaultTheme="light"
// Feel free to remove this line if you want to use the system theme by default
@ -43,8 +35,6 @@ export default async function RootLayout({
>
<div className="flex min-h-screen flex-col items-center justify-center">
<Navbar
user={user}
isLoggedIn={!!user}
links={[
{
name: "Agent Store",

View File

@ -1,9 +1,10 @@
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { createServerClient } from "@/lib/supabase/server";
import { z } from "zod";
import * as Sentry from "@sentry/nextjs";
import getServerSupabase from "@/lib/supabase/getServerSupabase";
import BackendAPI from "@/lib/autogpt-server-api";
const loginFormSchema = z.object({
email: z.string().email().min(2).max(64),
@ -15,7 +16,7 @@ export async function logout() {
"logout",
{},
async () => {
const supabase = createServerClient();
const supabase = getServerSupabase();
if (!supabase) {
redirect("/error");
@ -36,7 +37,8 @@ export async function logout() {
export async function login(values: z.infer<typeof loginFormSchema>) {
return await Sentry.withServerActionInstrumentation("login", {}, async () => {
const supabase = createServerClient();
const supabase = getServerSupabase();
const api = new BackendAPI();
if (!supabase) {
redirect("/error");
@ -45,6 +47,8 @@ export async function login(values: z.infer<typeof loginFormSchema>) {
// We are sure that the values are of the correct type because zod validates the form
const { data, error } = await supabase.auth.signInWithPassword(values);
await api.createUser();
if (error) {
console.log("Error logging in", error);
if (error.status == 400) {
@ -70,7 +74,7 @@ export async function signup(values: z.infer<typeof loginFormSchema>) {
"signup",
{},
async () => {
const supabase = createServerClient();
const supabase = getServerSupabase();
if (!supabase) {
redirect("/error");

View File

@ -1,5 +1,4 @@
"use client";
import useUser from "@/hooks/useUser";
import { login, signup } from "./actions";
import { Button } from "@/components/ui/button";
import {
@ -18,10 +17,12 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { PasswordInput } from "@/components/PasswordInput";
import { FaGoogle, FaGithub, FaDiscord, FaSpinner } from "react-icons/fa";
import { useState } from "react";
import { useSupabase } from "@/components/providers/SupabaseProvider";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { Checkbox } from "@/components/ui/checkbox";
import useSupabase from "@/hooks/useSupabase";
import Spinner from "@/components/Spinner";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
const loginFormSchema = z.object({
email: z.string().email().min(2).max(64),
@ -32,11 +33,11 @@ const loginFormSchema = z.object({
});
export default function LoginPage() {
const { supabase, isLoading: isSupabaseLoading } = useSupabase();
const { user, isLoading: isUserLoading } = useUser();
const { supabase, user, isUserLoading } = useSupabase();
const [feedback, setFeedback] = useState<string | null>(null);
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const api = useBackendAPI();
const form = useForm<z.infer<typeof loginFormSchema>>({
resolver: zodResolver(loginFormSchema),
@ -48,16 +49,12 @@ export default function LoginPage() {
});
if (user) {
console.log("User exists, redirecting to home");
console.debug("User exists, redirecting to /");
router.push("/");
}
if (isUserLoading || isSupabaseLoading || user) {
return (
<div className="flex h-[80vh] items-center justify-center">
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
</div>
);
if (isUserLoading || user) {
return <Spinner />;
}
if (!supabase) {
@ -80,6 +77,8 @@ export default function LoginPage() {
},
});
await api.createUser();
if (!error) {
setFeedback(null);
return;

View File

@ -1,11 +1,7 @@
"use client";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import AutoGPTServerAPI, {
GraphExecution,
Schedule,
GraphMeta,
} from "@/lib/autogpt-server-api";
import { GraphExecution, Schedule, GraphMeta } from "@/lib/autogpt-server-api";
import { Card } from "@/components/ui/card";
import {
@ -16,6 +12,7 @@ import {
FlowRunsStats,
} from "@/components/monitor";
import { SchedulesTable } from "@/components/monitor/scheduleTable";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
const Monitor = () => {
const [flows, setFlows] = useState<GraphMeta[]>([]);
@ -25,8 +22,7 @@ const Monitor = () => {
const [selectedRun, setSelectedRun] = useState<GraphExecution | null>(null);
const [sortColumn, setSortColumn] = useState<keyof Schedule>("id");
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
const api = useMemo(() => new AutoGPTServerAPI(), []);
const api = useBackendAPI();
const fetchSchedules = useCallback(async () => {
setSchedules(await api.listSchedules());

View File

@ -1,11 +1,7 @@
"use client";
import { useSupabase } from "@/components/providers/SupabaseProvider";
import { Button } from "@/components/ui/button";
import useUser from "@/hooks/useUser";
import { useRouter } from "next/navigation";
import { useCallback, useContext, useMemo, useState } from "react";
import { FaSpinner } from "react-icons/fa";
import { Separator } from "@/components/ui/separator";
import { useToast } from "@/components/ui/use-toast";
import { IconKey, IconUser } from "@/components/ui/icons";
@ -31,10 +27,11 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import useSupabase from "@/hooks/useSupabase";
import Spinner from "@/components/Spinner";
export default function PrivatePage() {
const { user, isLoading, error } = useUser();
const { supabase } = useSupabase();
const { supabase, user, isUserLoading } = useSupabase();
const router = useRouter();
const providers = useContext(CredentialsProvidersContext);
const { toast } = useToast();
@ -115,30 +112,28 @@ export default function PrivatePage() {
[],
);
if (isLoading || !providers) {
return (
<div className="flex h-[80vh] items-center justify-center">
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
</div>
);
if (isUserLoading) {
return <Spinner />;
}
if (error || !user || !supabase) {
if (!user || !supabase) {
router.push("/login");
return null;
}
const allCredentials = Object.values(providers).flatMap((provider) =>
[...provider.savedOAuthCredentials, ...provider.savedApiKeys]
.filter((cred) => !hiddenCredentials.includes(cred.id))
.map((credentials) => ({
...credentials,
provider: provider.provider,
providerName: provider.providerName,
ProviderIcon: providerIcons[provider.provider],
TypeIcon: { oauth2: IconUser, api_key: IconKey }[credentials.type],
})),
);
const allCredentials = providers
? Object.values(providers).flatMap((provider) =>
[...provider.savedOAuthCredentials, ...provider.savedApiKeys]
.filter((cred) => !hiddenCredentials.includes(cred.id))
.map((credentials) => ({
...credentials,
provider: provider.provider,
providerName: provider.providerName,
ProviderIcon: providerIcons[provider.provider],
TypeIcon: { oauth2: IconUser, api_key: IconKey }[credentials.type],
})),
)
: [];
return (
<div className="mx-auto max-w-3xl md:py-8">

View File

@ -5,27 +5,19 @@ 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 SupabaseProvider from "@/components/providers/SupabaseProvider";
import CredentialsProvider from "@/components/integrations/credentials-provider";
import { User } from "@supabase/supabase-js";
import { LaunchDarklyProvider } from "@/components/feature-flag/feature-flag-provider";
export function Providers({
children,
initialUser,
...props
}: ThemeProviderProps & { initialUser: User | null }) {
export function Providers({ children, ...props }: ThemeProviderProps) {
return (
<NextThemesProvider {...props}>
<SupabaseProvider initialUser={initialUser}>
<BackendAPIProvider>
<CredentialsProvider>
<LaunchDarklyProvider>
<TooltipProvider>{children}</TooltipProvider>
</LaunchDarklyProvider>
</CredentialsProvider>
</BackendAPIProvider>
</SupabaseProvider>
<BackendAPIProvider>
<CredentialsProvider>
<LaunchDarklyProvider>
<TooltipProvider>{children}</TooltipProvider>
</LaunchDarklyProvider>
</CredentialsProvider>
</BackendAPIProvider>
</NextThemesProvider>
);
}

View File

@ -1,5 +1,4 @@
"use client";
import { useSupabase } from "@/components/providers/SupabaseProvider";
import { Button } from "@/components/ui/button";
import {
Form,
@ -10,7 +9,7 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import useUser from "@/hooks/useUser";
import useSupabase from "@/hooks/useSupabase";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { useState } from "react";
@ -33,8 +32,7 @@ const resetPasswordFormSchema = z
});
export default function ResetPasswordPage() {
const { supabase, isLoading: isSupabaseLoading } = useSupabase();
const { user, isLoading: isUserLoading } = useUser();
const { supabase, user, isUserLoading } = useSupabase();
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [feedback, setFeedback] = useState<string | null>(null);
@ -54,7 +52,7 @@ export default function ResetPasswordPage() {
},
});
if (isUserLoading || isSupabaseLoading) {
if (isUserLoading) {
return (
<div className="flex h-[80vh] items-center justify-center">
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />

View File

@ -5,8 +5,6 @@ import { AgentTable } from "@/components/agptui/AgentTable";
import { AgentTableRowProps } from "@/components/agptui/AgentTableRow";
import { Button } from "@/components/agptui/Button";
import { Separator } from "@/components/ui/separator";
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
import { createClient } from "@/lib/supabase/client";
import { StatusType } from "@/components/agptui/Status";
import { PublishAgentPopout } from "@/components/agptui/composite/PublishAgentPopout";
import { useCallback, useEffect, useState } from "react";
@ -14,42 +12,12 @@ import {
StoreSubmissionsResponse,
StoreSubmissionRequest,
} from "@/lib/autogpt-server-api/types";
async function getDashboardData() {
const supabase = createClient();
if (!supabase) {
return { submissions: [] };
}
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
console.warn("--- No session found in profile page");
return { profile: null };
}
const api = new AutoGPTServerAPI(
process.env.NEXT_PUBLIC_AGPT_SERVER_URL,
process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL,
supabase,
);
try {
const submissions = await api.getStoreSubmissions();
return {
submissions,
};
} catch (error) {
console.error("Error fetching profile:", error);
return {
profile: null,
};
}
}
import useSupabase from "@/hooks/useSupabase";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
export default function Page({}: {}) {
const { supabase } = useSupabase();
const api = useBackendAPI();
const [submissions, setSubmissions] = useState<StoreSubmissionsResponse>();
const [openPopout, setOpenPopout] = useState<boolean>(false);
const [submissionData, setSubmissionData] =
@ -59,15 +27,20 @@ export default function Page({}: {}) {
);
const fetchData = useCallback(async () => {
const { submissions } = await getDashboardData();
if (submissions) {
setSubmissions(submissions as StoreSubmissionsResponse);
try {
const submissions = await api.getStoreSubmissions();
setSubmissions(submissions);
} catch (error) {
console.error("Error fetching submissions:", error);
}
}, []);
}, [api, supabase]);
useEffect(() => {
if (!supabase) {
return;
}
fetchData();
}, [fetchData]);
}, [supabase]);
const onEditSubmission = useCallback((submission: StoreSubmissionRequest) => {
setSubmissionData(submission);
@ -77,19 +50,13 @@ export default function Page({}: {}) {
const onDeleteSubmission = useCallback(
(submission_id: string) => {
const supabase = createClient();
if (!supabase) {
return;
}
const api = new AutoGPTServerAPI(
process.env.NEXT_PUBLIC_AGPT_SERVER_URL,
process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL,
supabase,
);
api.deleteStoreSubmission(submission_id);
fetchData();
},
[fetchData],
[supabase],
);
const onOpenPopout = useCallback(() => {

View File

@ -1,28 +1,9 @@
import * as React from "react";
import { ProfileInfoForm } from "@/components/agptui/ProfileInfoForm";
import AutoGPTServerAPIServerSide from "@/lib/autogpt-server-api";
import { createServerClient } from "@/lib/supabase/server";
import BackendAPI from "@/lib/autogpt-server-api";
import { CreatorDetails } from "@/lib/autogpt-server-api/types";
async function getProfileData() {
// Get the supabase client first
const supabase = createServerClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
console.warn("--- No session found in profile page");
return { profile: null };
}
// Create API client with the same supabase instance
const api = new AutoGPTServerAPIServerSide(
process.env.NEXT_PUBLIC_AGPT_SERVER_URL,
process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL,
supabase, // Pass the supabase client instance
);
async function getProfileData(api: BackendAPI) {
try {
const profile = await api.getStoreProfile("profile");
return {
@ -37,7 +18,8 @@ async function getProfileData() {
}
export default async function Page({}: {}) {
const { profile } = await getProfileData();
const api = new BackendAPI();
const { profile } = await getProfileData(api);
if (!profile) {
return (

View File

@ -1,4 +1,4 @@
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
import BackendAPI from "@/lib/autogpt-server-api";
import { BreadCrumbs } from "@/components/agptui/BreadCrumbs";
import { AgentInfo } from "@/components/agptui/AgentInfo";
import { AgentImages } from "@/components/agptui/AgentImages";
@ -12,7 +12,7 @@ export async function generateMetadata({
}: {
params: { creator: string; slug: string };
}): Promise<Metadata> {
const api = new AutoGPTServerAPI();
const api = new BackendAPI();
const agent = await api.getStoreAgent(params.creator, params.slug);
return {
@ -22,7 +22,7 @@ export async function generateMetadata({
}
// export async function generateStaticParams() {
// const api = new AutoGPTServerAPI();
// const api = new BackendAPI();
// const agents = await api.getStoreAgents({ featured: true });
// return agents.agents.map((agent) => ({
// creator: agent.creator,
@ -35,7 +35,7 @@ export default async function Page({
}: {
params: { creator: string; slug: string };
}) {
const api = new AutoGPTServerAPI();
const api = new BackendAPI();
const agent = await api.getStoreAgent(params.creator, params.slug);
const otherAgents = await api.getStoreAgents({ creator: params.creator });
const similarAgents = await api.getStoreAgents({

View File

@ -1,4 +1,4 @@
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
import BackendAPI from "@/lib/autogpt-server-api";
import {
CreatorDetails as Creator,
StoreAgent,
@ -14,7 +14,7 @@ export async function generateMetadata({
}: {
params: { creator: string };
}): Promise<Metadata> {
const api = new AutoGPTServerAPI();
const api = new BackendAPI();
const creator = await api.getStoreCreator(params.creator);
return {
@ -24,7 +24,7 @@ export async function generateMetadata({
}
// export async function generateStaticParams() {
// const api = new AutoGPTServerAPI();
// const api = new BackendAPI();
// const creators = await api.getStoreCreators({ featured: true });
// return creators.creators.map((creator) => ({
// creator: creator.username,
@ -36,7 +36,7 @@ export default async function Page({
}: {
params: { creator: string };
}) {
const api = new AutoGPTServerAPI();
const api = new BackendAPI();
try {
const creator = await api.getStoreCreator(params.creator);

View File

@ -14,28 +14,18 @@ import {
FeaturedCreator,
} from "@/components/agptui/composite/FeaturedCreators";
import { Separator } from "@/components/ui/separator";
import AutoGPTServerAPIServerSide from "@/lib/autogpt-server-api/clientServer";
import { Metadata } from "next";
import { createServerClient } from "@/lib/supabase/server";
import {
StoreAgentsResponse,
CreatorsResponse,
} from "@/lib/autogpt-server-api/types";
import BackendAPI from "@/lib/autogpt-server-api";
export const dynamic = "force-dynamic";
async function getStoreData() {
try {
const supabase = createServerClient();
const {
data: { session },
} = await supabase.auth.getSession();
const api = new AutoGPTServerAPIServerSide(
process.env.NEXT_PUBLIC_AGPT_SERVER_URL,
process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL,
supabase,
);
const api = new BackendAPI();
// Add error handling and default values
let featuredAgents: StoreAgentsResponse = {

View File

@ -1,13 +1,13 @@
"use client";
import { useState, useEffect } from "react";
import { AutoGPTServerAPI } from "@/lib/autogpt-server-api/client";
import { AgentsSection } from "@/components/agptui/composite/AgentsSection";
import { SearchBar } from "@/components/agptui/SearchBar";
import { FeaturedCreators } from "@/components/agptui/composite/FeaturedCreators";
import { Separator } from "@/components/ui/separator";
import { SearchFilterChips } from "@/components/agptui/SearchFilterChips";
import { SortDropdown } from "@/components/agptui/SortDropdown";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
export default function Page({
searchParams,
@ -34,11 +34,11 @@ function SearchResults({
const [agents, setAgents] = useState<any[]>([]);
const [creators, setCreators] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const api = useBackendAPI();
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
const api = new AutoGPTServerAPI();
try {
const [agentsRes, creatorsRes] = await Promise.all([

View File

@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button";
import React from "react";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import Image from "next/image";
import getServerUser from "@/hooks/getServerUser";
import getServerUser from "@/lib/supabase/getServerUser";
import ProfileDropdown from "./ProfileDropdown";
import { IconCircleUser, IconMenu } from "@/components/ui/icons";
import CreditButton from "@/components/nav/CreditButton";

View File

@ -7,16 +7,14 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "./ui/button";
import { useSupabase } from "./providers/SupabaseProvider";
import { useRouter } from "next/navigation";
import useUser from "@/hooks/useUser";
import useSupabase from "@/hooks/useSupabase";
const ProfileDropdown = () => {
const { supabase } = useSupabase();
const { supabase, user, isUserLoading } = useSupabase();
const router = useRouter();
const { user, role, isLoading } = useUser();
if (isLoading) {
if (isUserLoading) {
return null;
}
@ -37,7 +35,7 @@ const ProfileDropdown = () => {
<DropdownMenuItem onClick={() => router.push("/profile")}>
Profile
</DropdownMenuItem>
{role === "admin" && (
{user!.role === "admin" && (
<DropdownMenuItem onClick={() => router.push("/admin/dashboard")}>
Admin Dashboard
</DropdownMenuItem>

View File

@ -1,6 +1,6 @@
// components/RoleBasedAccess.tsx
import useSupabase from "@/hooks/useSupabase";
import React from "react";
import useUser from "@/hooks/useUser";
interface RoleBasedAccessProps {
allowedRoles: string[];
@ -11,13 +11,13 @@ const RoleBasedAccess: React.FC<RoleBasedAccessProps> = ({
allowedRoles,
children,
}) => {
const { role, isLoading } = useUser();
const { user, isUserLoading } = useSupabase();
if (isLoading) {
if (isUserLoading) {
return <div>Loading...</div>;
}
if (!role || !allowedRoles.includes(role)) {
if (!user!.role || !allowedRoles.includes(user!.role)) {
return null;
}

View File

@ -0,0 +1,9 @@
import { FaSpinner } from "react-icons/fa";
export default function Spinner() {
return (
<div className="flex h-[80vh] items-center justify-center">
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
</div>
);
}

View File

@ -3,9 +3,19 @@
// import ServerSideMarketplaceAPI from "@/lib/marketplace-api/server-client";
// import { revalidatePath } from "next/cache";
// import * as Sentry from "@sentry/nextjs";
// import { checkAuth, createServerClient } from "@/lib/supabase/server";
// import { redirect } from "next/navigation";
// import { createClient } from "@/lib/supabase/client";
// export async function checkAuth() {
// const supabase = getServerSupabase();
// if (!supabase) {
// console.error("No supabase client");
// redirect("/login");
// }
// const { data, error } = await supabase.auth.getUser();
// if (error || !data?.user) {
// redirect("/login");
// }
// }
// export async function approveAgent(
// agentId: string,

View File

@ -14,12 +14,10 @@ import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import AutoGPTServerAPI, {
Graph,
GraphCreatable,
} from "@/lib/autogpt-server-api";
import { Graph, GraphCreatable } from "@/lib/autogpt-server-api";
import { cn } from "@/lib/utils";
import { EnterIcon } from "@radix-ui/react-icons";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
// Add this custom schema for File type
const fileSchema = z.custom<File>((val) => val instanceof File, {
@ -75,7 +73,7 @@ export const AgentImportForm: React.FC<
React.FormHTMLAttributes<HTMLFormElement>
> = ({ className, ...props }) => {
const [agentObject, setAgentObject] = useState<GraphCreatable | null>(null);
const api = new AutoGPTServerAPI();
const api = useBackendAPI();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),

View File

@ -1,13 +1,13 @@
"use client";
import { IconRefresh } from "@/components/ui/icons";
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
import { useState } from "react";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
interface CreditsCardProps {
credits: number;
@ -15,7 +15,7 @@ interface CreditsCardProps {
const CreditsCard = ({ credits }: CreditsCardProps) => {
const [currentCredits, setCurrentCredits] = useState(credits);
const api = new AutoGPTServerAPI();
const api = useBackendAPI();
const onRefresh = async () => {
const { credits } = await api.getUserCredit("credits-card");

View File

@ -6,10 +6,10 @@ import { MobileNavBar } from "./MobileNavBar";
import { Button } from "./Button";
import CreditsCard from "./CreditsCard";
import { ProfileDetails } from "@/lib/autogpt-server-api/types";
import { User } from "@supabase/supabase-js";
import AutoGPTServerAPIServerSide from "@/lib/autogpt-server-api/clientServer";
import { ThemeToggle } from "./ThemeToggle";
import { NavbarLink } from "./NavbarLink";
import getServerUser from "@/lib/supabase/getServerUser";
import BackendAPI from "@/lib/autogpt-server-api";
interface NavLink {
name: string;
@ -17,8 +17,6 @@ interface NavLink {
}
interface NavbarProps {
user: User | null;
isLoggedIn: boolean;
links: NavLink[];
menuItemGroups: {
groupName?: string;
@ -31,8 +29,8 @@ interface NavbarProps {
}[];
}
async function getProfileData(user: User | null) {
const api = new AutoGPTServerAPIServerSide();
async function getProfileData() {
const api = new BackendAPI();
const [profile, credits] = await Promise.all([
api.getStoreProfile("navbar"),
api.getUserCredit("navbar"),
@ -43,17 +41,14 @@ async function getProfileData(user: User | null) {
credits,
};
}
export const Navbar = async ({
user,
isLoggedIn,
links,
menuItemGroups,
}: NavbarProps) => {
export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => {
const { user } = await getServerUser();
const isLoggedIn = user !== null;
let profile: ProfileDetails | null = null;
let credits: { credits: number } = { credits: 0 };
if (isLoggedIn) {
const { profile: t_profile, credits: t_credits } =
await getProfileData(user);
const { profile: t_profile, credits: t_credits } = await getProfileData();
profile = t_profile;
credits = t_credits;
}

View File

@ -6,25 +6,17 @@ import { useState } from "react";
import Image from "next/image";
import { Button } from "./Button";
import { IconPersonFill } from "@/components/ui/icons";
import { AutoGPTServerAPI } from "@/lib/autogpt-server-api/client";
import { CreatorDetails, ProfileDetails } from "@/lib/autogpt-server-api/types";
import { createClient } from "@/lib/supabase/client";
import { Separator } from "@/components/ui/separator";
import useSupabase from "@/hooks/useSupabase";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
export const ProfileInfoForm = ({ profile }: { profile: CreatorDetails }) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const [profileData, setProfileData] = useState(profile);
const supabase = createClient();
const api = new AutoGPTServerAPI(
process.env.NEXT_PUBLIC_AGPT_SERVER_URL,
process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL,
supabase,
);
const { supabase } = useSupabase();
const api = useBackendAPI();
const submitForm = async () => {
try {

View File

@ -4,8 +4,7 @@ import * as React from "react";
import Image from "next/image";
import { Button } from "../agptui/Button";
import { IconClose, IconPlus } from "../ui/icons";
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
import { createClient } from "@/lib/supabase/client";
import BackendAPI from "@/lib/autogpt-server-api";
interface PublishAgentInfoProps {
onBack: () => void;
@ -96,11 +95,7 @@ export const PublishAgentInfo: React.FC<PublishAgentInfoProps> = ({
if (!file) return;
try {
const api = new AutoGPTServerAPI(
process.env.NEXT_PUBLIC_AGPT_SERVER_URL,
process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL,
createClient(),
);
const api = new BackendAPI();
const imageUrl = (await api.uploadStoreSubmissionMedia(file)).replace(
/^"(.*)"$/,

View File

@ -3,8 +3,7 @@
import * as React from "react";
import { Cross1Icon } from "@radix-ui/react-icons";
import { IconStar, IconStarFilled } from "@/components/ui/icons";
import { createClient } from "@/lib/supabase/client";
import { AutoGPTServerAPI } from "@/lib/autogpt-server-api/client";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
interface RatingCardProps {
agentName: string;
@ -18,18 +17,7 @@ export const RatingCard: React.FC<RatingCardProps> = ({
const [rating, setRating] = React.useState<number>(0);
const [hoveredRating, setHoveredRating] = React.useState<number>(0);
const [isVisible, setIsVisible] = React.useState(true);
const supabase = React.useMemo(() => createClient(), []);
const api = React.useMemo(
() =>
new AutoGPTServerAPI(
process.env.NEXT_PUBLIC_AGPT_SERVER_URL,
process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL,
supabase,
),
[supabase],
);
const api = useBackendAPI();
const handleClose = () => {
setIsVisible(false);

View File

@ -2,8 +2,7 @@
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { createClient } from "@/lib/supabase/client";
import useSupabase from "@/hooks/useSupabase";
interface SettingsInputFormProps {
email?: string;
@ -20,6 +19,7 @@ export const SettingsInputForm = ({
const [password, setPassword] = React.useState("");
const [confirmPassword, setConfirmPassword] = React.useState("");
const [passwordsMatch, setPasswordsMatch] = React.useState(true);
const { supabase } = useSupabase();
const handleSaveChanges = async () => {
if (password !== confirmPassword) {
@ -27,10 +27,9 @@ export const SettingsInputForm = ({
return;
}
setPasswordsMatch(true);
const client = createClient();
if (client) {
if (supabase) {
try {
const { error } = await client.auth.updateUser({
const { error } = await supabase.auth.updateUser({
password: password,
});
if (error) {

View File

@ -7,7 +7,7 @@ import {
PopoverContent,
PopoverAnchor,
} from "@/components/ui/popover";
import { PublishAgentSelect, Agent } from "../PublishAgentSelect";
import { PublishAgentSelect } from "../PublishAgentSelect";
import { PublishAgentInfo } from "../PublishAgentSelectInfo";
import { PublishAgentAwaitingReview } from "../PublishAgentAwaitingReview";
import { Button } from "../Button";
@ -15,9 +15,8 @@ import {
StoreSubmissionRequest,
MyAgentsResponse,
} from "@/lib/autogpt-server-api";
import { createClient } from "@/lib/supabase/client";
import { AutoGPTServerAPI } from "@/lib/autogpt-server-api/client";
import { useRouter } from "next/navigation";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
interface PublishAgentPopoutProps {
trigger?: React.ReactNode;
openPopout?: boolean;
@ -57,18 +56,7 @@ export const PublishAgentPopout: React.FC<PublishAgentPopoutProps> = ({
const popupId = React.useId();
const router = useRouter();
const supabase = React.useMemo(() => createClient(), []);
const api = React.useMemo(
() =>
new AutoGPTServerAPI(
process.env.NEXT_PUBLIC_AGPT_SERVER_URL,
process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL,
supabase,
),
[supabase],
);
const api = useBackendAPI();
React.useEffect(() => {
console.log("PublishAgentPopout Effect");

View File

@ -6,10 +6,9 @@ import { Button } from "@/components/ui/button";
import SchemaTooltip from "@/components/SchemaTooltip";
import useCredentials from "@/hooks/useCredentials";
import { zodResolver } from "@hookform/resolvers/zod";
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
import { NotionLogoIcon } from "@radix-ui/react-icons";
import { FaDiscord, FaGithub, FaGoogle, FaMedium, FaKey } from "react-icons/fa";
import { FC, useMemo, useState } from "react";
import { FC, useState } from "react";
import {
CredentialsMetaInput,
CredentialsProviderName,
@ -39,6 +38,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
const fallbackIcon = FaKey;
@ -91,7 +91,7 @@ export const CredentialsInput: FC<{
selectedCredentials?: CredentialsMetaInput;
onSelectCredentials: (newValue?: CredentialsMetaInput) => void;
}> = ({ className, selectedCredentials, onSelectCredentials }) => {
const api = useMemo(() => new AutoGPTServerAPI(), []);
const api = useBackendAPI();
const credentials = useCredentials();
const [isAPICredentialsModalOpen, setAPICredentialsModalOpen] =
useState(false);

View File

@ -1,4 +1,4 @@
import AutoGPTServerAPI, {
import {
APIKeyCredentials,
CredentialsDeleteNeedConfirmationResponse,
CredentialsDeleteResponse,
@ -6,13 +6,8 @@ import AutoGPTServerAPI, {
CredentialsProviderName,
PROVIDER_NAMES,
} from "@/lib/autogpt-server-api";
import {
createContext,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { createContext, useCallback, useEffect, useState } from "react";
// Get keys from CredentialsProviderName type
const CREDENTIALS_PROVIDER_NAMES = Object.values(
@ -87,7 +82,7 @@ export default function CredentialsProvider({
}) {
const [providers, setProviders] =
useState<CredentialsProvidersContextType | null>(null);
const api = useMemo(() => new AutoGPTServerAPI(), []);
const api = useBackendAPI();
const addCredentials = useCallback(
(
@ -120,7 +115,7 @@ export default function CredentialsProvider({
[setProviders],
);
/** Wraps `AutoGPTServerAPI.oAuthCallback`, and adds the result to the internal credentials store. */
/** Wraps `BackendAPI.oAuthCallback`, and adds the result to the internal credentials store. */
const oAuthCallback = useCallback(
async (
provider: CredentialsProviderName,
@ -134,7 +129,7 @@ export default function CredentialsProvider({
[api, addCredentials],
);
/** Wraps `AutoGPTServerAPI.createAPIKeyCredentials`, and adds the result to the internal credentials store. */
/** Wraps `BackendAPI.createAPIKeyCredentials`, and adds the result to the internal credentials store. */
const createAPIKeyCredentials = useCallback(
async (
provider: CredentialsProviderName,
@ -150,7 +145,7 @@ export default function CredentialsProvider({
[api, addCredentials],
);
/** Wraps `AutoGPTServerAPI.deleteCredentials`, and removes the credentials from the internal store. */
/** Wraps `BackendAPI.deleteCredentials`, and removes the credentials from the internal store. */
const deleteCredentials = useCallback(
async (
provider: CredentialsProviderName,

View File

@ -2,7 +2,7 @@
// import Link from "next/link";
// import { ArrowLeft, Download, Calendar, Tag } from "lucide-react";
// import { Button } from "@/components/ui/button";
// import AutoGPTServerAPI, { GraphCreatable } from "@/lib/autogpt-server-api";
// import BackendAPI, { GraphCreatable } from "@/lib/autogpt-server-api";
// import "@xyflow/react/dist/style.css";
// import { useToast } from "../ui/use-toast";

View File

@ -1,4 +1,4 @@
import AutoGPTServerAPI, {
import BackendAPI, {
GraphExecution,
GraphMeta,
} from "@/lib/autogpt-server-api";
@ -45,8 +45,6 @@ export const AgentFlowList = ({
onSelectFlow: (f: GraphMeta) => void;
className?: string;
}) => {
const api = useMemo(() => new AutoGPTServerAPI(), []);
return (
<Card className={className}>
<CardHeader className="flex-row items-center justify-between space-x-3 space-y-0">

View File

@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useState, useCallback } from "react";
import AutoGPTServerAPI, {
import React, { useEffect, useState, useCallback } from "react";
import {
GraphExecution,
Graph,
GraphMeta,
@ -22,7 +22,7 @@ import { ClockIcon, ExitIcon, Pencil2Icon } from "@radix-ui/react-icons";
import Link from "next/link";
import { exportAsJSONFile, filterBlocksByType } from "@/lib/utils";
import { FlowRunsStats } from "@/components/monitor/index";
import { Trash2Icon, Timer } from "lucide-react";
import { Trash2Icon } from "lucide-react";
import {
Dialog,
DialogContent,
@ -35,6 +35,7 @@ import { useToast } from "@/components/ui/use-toast";
import { CronScheduler } from "@/components/cronScheduler";
import RunnerInputUI from "@/components/runner-ui/RunnerInputUI";
import useAgentGraph from "@/hooks/useAgentGraph";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
export const FlowInfo: React.FC<
React.HTMLAttributes<HTMLDivElement> & {
@ -66,7 +67,7 @@ export const FlowInfo: React.FC<
setEdges,
} = useAgentGraph(flow.id, false);
const api = useMemo(() => new AutoGPTServerAPI(), []);
const api = useBackendAPI();
const { toast } = useToast();
const [flowVersions, setFlowVersions] = useState<Graph[] | null>(null);

View File

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import AutoGPTServerAPI, {
import React, { useCallback, useEffect, useState } from "react";
import {
GraphExecution,
GraphMeta,
NodeExecutionResult,
@ -13,6 +13,7 @@ import { ExitIcon, Pencil2Icon } from "@radix-ui/react-icons";
import moment from "moment/moment";
import { FlowRunStatusBadge } from "@/components/monitor/FlowRunStatusBadge";
import RunnerOutputUI, { BlockOutput } from "../runner-ui/RunnerOutputUI";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
export const FlowRunInfo: React.FC<
React.HTMLAttributes<HTMLDivElement> & {
@ -22,7 +23,7 @@ export const FlowRunInfo: React.FC<
> = ({ flow, execution, ...props }) => {
const [isOutputOpen, setIsOutputOpen] = useState(false);
const [blockOutputs, setBlockOutputs] = useState<BlockOutput[]>([]);
const api = useMemo(() => new AutoGPTServerAPI(), []);
const api = useBackendAPI();
const fetchBlockResults = useCallback(async () => {
const executionResults = await api.getGraphExecutionInfo(
@ -119,7 +120,7 @@ export const FlowRunInfo: React.FC<
<strong>Agent ID:</strong> <code>{flow.id}</code>
</p>
<p className="hidden">
<strong>Run ID:</strong> <code>{flowRun.id}</code>
<strong>Run ID:</strong> <code>{execution.execution_id}</code>
</p>
<div>
<strong>Status:</strong>{" "}

View File

@ -3,12 +3,11 @@
import { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { IconRefresh } from "@/components/ui/icons";
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
const api = new AutoGPTServerAPI();
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
export default function CreditButton() {
const [credit, setCredit] = useState<number | null>(null);
const api = useBackendAPI();
const fetchCredit = useCallback(async () => {
try {

View File

@ -1,75 +0,0 @@
"use client";
import { createClient } from "@/lib/supabase/client";
import { SupabaseClient, User } from "@supabase/supabase-js";
import { Session } from "@supabase/supabase-js";
import { useRouter } from "next/navigation";
import { createContext, useContext, useEffect, useState } from "react";
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
type SupabaseContextType = {
supabase: SupabaseClient | null;
isLoading: boolean;
user: User | null;
};
const Context = createContext<SupabaseContextType | undefined>(undefined);
export default function SupabaseProvider({
children,
initialUser,
}: {
children: React.ReactNode;
initialUser: User | null;
}) {
const [user, setUser] = useState<User | null>(initialUser);
const [supabase, setSupabase] = useState<SupabaseClient | null>(null);
const [isLoading, setIsLoading] = useState(true);
const router = useRouter();
useEffect(() => {
const initializeSupabase = async () => {
setIsLoading(true);
const client = createClient();
const api = new AutoGPTServerAPI();
setSupabase(client);
setIsLoading(false);
if (client) {
const {
data: { subscription },
} = client.auth.onAuthStateChange((event, session) => {
client.auth.getUser().then((user) => {
setUser(user.data.user);
});
if (event === "SIGNED_IN") {
api.createUser();
}
if (event === "SIGNED_OUT") {
router.refresh();
}
});
return () => {
subscription.unsubscribe();
};
}
};
initializeSupabase();
}, [router]);
return (
<Context.Provider value={{ supabase, isLoading, user }}>
{children}
</Context.Provider>
);
}
export const useSupabase = () => {
const context = useContext(Context);
if (context === undefined) {
throw new Error("useSupabase must be used inside SupabaseProvider");
}
return context;
};

View File

@ -1,6 +1,6 @@
import { CustomEdge } from "@/components/CustomEdge";
import { CustomNode } from "@/components/CustomNode";
import AutoGPTServerAPI, {
import BackendAPI, {
Block,
BlockIOSubSchema,
BlockUIType,
@ -74,7 +74,7 @@ export default function useAgentGraph(
const [edges, setEdges] = useState<CustomEdge[]>([]);
const api = useMemo(
() => new AutoGPTServerAPI(process.env.NEXT_PUBLIC_AGPT_SERVER_URL!),
() => new BackendAPI(process.env.NEXT_PUBLIC_AGPT_SERVER_URL!),
[],
);

View File

@ -0,0 +1,42 @@
import { createBrowserClient } from "@supabase/ssr";
import { User } from "@supabase/supabase-js";
import { useEffect, useMemo, useState } from "react";
export default function useSupabase() {
const [user, setUser] = useState<User | null>(null);
const [isUserLoading, setIsUserLoading] = useState(true);
const supabase = useMemo(() => {
try {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
} catch (error) {
console.error("Error creating Supabase client", error);
return null;
}
}, []);
useEffect(() => {
if (!supabase) {
setIsUserLoading(false);
return;
}
const fetchUser = async () => {
const response = await supabase.auth.getUser();
if (response.error) {
console.error("Error fetching user", response.error);
} else {
setUser(response.data.user);
}
setIsUserLoading(false);
};
fetchUser();
}, [supabase]);
return { supabase, user, isUserLoading };
}

View File

@ -1,67 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import { User, Session } from "@supabase/supabase-js";
import { useSupabase } from "@/components/providers/SupabaseProvider";
const useUser = () => {
const { supabase, isLoading: isSupabaseLoading } = useSupabase();
const [user, setUser] = useState<User | null>(null);
const [session, setSession] = useState<Session | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [role, setRole] = useState<string | null>(null);
useEffect(() => {
if (isSupabaseLoading || !supabase) {
return;
}
const fetchUser = async () => {
try {
setIsLoading(true);
const { data: userData, error: userError } =
await supabase.auth.getUser();
const { data: sessionData, error: sessionError } =
await supabase.auth.getSession();
if (userError) throw new Error(`User error: ${userError.message}`);
if (sessionError)
throw new Error(`Session error: ${sessionError.message}`);
setUser(userData.user);
setSession(sessionData.session);
setRole(userData.user?.role || null);
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to fetch user data");
console.error("Error in useUser hook:", e);
} finally {
setIsLoading(false);
}
};
fetchUser();
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
setUser(session?.user ?? null);
setRole(session?.user?.role || null);
setIsLoading(false);
});
return () => subscription.unsubscribe();
}, [supabase, isSupabaseLoading]);
return {
user,
session,
role,
isLoading: isLoading || isSupabaseLoading,
error,
};
};
export default useUser;

View File

@ -1,659 +0,0 @@
import { SupabaseClient } from "@supabase/supabase-js";
import {
AnalyticsDetails,
AnalyticsMetrics,
APIKeyCredentials,
Block,
CredentialsDeleteNeedConfirmationResponse,
CredentialsDeleteResponse,
CredentialsMetaResponse,
GraphExecution,
Graph,
GraphCreatable,
GraphExecuteResponse,
GraphMeta,
GraphUpdateable,
NodeExecutionResult,
MyAgentsResponse,
OAuth2Credentials,
ProfileDetails,
User,
StoreAgentsResponse,
StoreAgentDetails,
CreatorsResponse,
CreatorDetails,
StoreSubmissionsResponse,
StoreSubmissionRequest,
StoreSubmission,
StoreReviewCreate,
StoreReview,
ScheduleCreatable,
Schedule,
} from "./types";
export default class BaseAutoGPTServerAPI {
private baseUrl: string;
private wsUrl: string;
private webSocket: WebSocket | null = null;
private wsConnecting: Promise<void> | null = null;
private wsMessageHandlers: Record<string, Set<(data: any) => void>> = {};
private supabaseClient: SupabaseClient | null = null;
heartbeatInterval: number | null = null;
readonly HEARTBEAT_INTERVAL = 10_0000; // 30 seconds
readonly HEARTBEAT_TIMEOUT = 10_000; // 10 seconds
heartbeatTimeoutId: number | null = null;
constructor(
baseUrl: string = process.env.NEXT_PUBLIC_AGPT_SERVER_URL ||
"http://localhost:8006/api/",
wsUrl: string = process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL ||
"ws://localhost:8001/ws",
supabaseClient: SupabaseClient | null = null,
) {
this.baseUrl = baseUrl;
this.wsUrl = wsUrl;
this.supabaseClient = supabaseClient;
}
async isAuthenticated(): Promise<boolean> {
if (!this.supabaseClient) return false;
const {
data: { session },
} = await this.supabaseClient?.auth.getSession();
return session != null;
}
createUser(): Promise<User> {
return this._request("POST", "/auth/user", {});
}
getUserCredit(page?: string): Promise<{ credits: number }> {
try {
return this._get(`/credits`, undefined, page);
} catch (error) {
return Promise.resolve({ credits: 0 });
}
}
getBlocks(): Promise<Block[]> {
return this._get("/blocks");
}
listGraphs(): Promise<GraphMeta[]> {
return this._get(`/graphs`);
}
getExecutions(): Promise<GraphExecution[]> {
return this._get(`/executions`);
}
getGraph(
id: string,
version?: number,
hide_credentials?: boolean,
): Promise<Graph> {
let query: Record<string, any> = {};
if (version !== undefined) {
query["version"] = version;
}
if (hide_credentials !== undefined) {
query["hide_credentials"] = hide_credentials;
}
return this._get(`/graphs/${id}`, query);
}
getGraphAllVersions(id: string): Promise<Graph[]> {
return this._get(`/graphs/${id}/versions`);
}
createGraph(graphCreateBody: GraphCreatable): Promise<Graph>;
createGraph(graphID: GraphCreatable | string): Promise<Graph> {
let requestBody = { graph: graphID } as GraphCreateRequestBody;
return this._request("POST", "/graphs", requestBody);
}
updateGraph(id: string, graph: GraphUpdateable): Promise<Graph> {
return this._request("PUT", `/graphs/${id}`, graph);
}
deleteGraph(id: string): Promise<void> {
return this._request("DELETE", `/graphs/${id}`);
}
setGraphActiveVersion(id: string, version: number): Promise<Graph> {
return this._request("PUT", `/graphs/${id}/versions/active`, {
active_graph_version: version,
});
}
executeGraph(
id: string,
inputData: { [key: string]: any } = {},
): Promise<GraphExecuteResponse> {
return this._request("POST", `/graphs/${id}/execute`, inputData);
}
async getGraphExecutionInfo(
graphID: string,
runID: string,
): Promise<NodeExecutionResult[]> {
return (await this._get(`/graphs/${graphID}/executions/${runID}`)).map(
parseNodeExecutionResultTimestamps,
);
}
async stopGraphExecution(
graphID: string,
runID: string,
): Promise<NodeExecutionResult[]> {
return (
await this._request("POST", `/graphs/${graphID}/executions/${runID}/stop`)
).map(parseNodeExecutionResultTimestamps);
}
oAuthLogin(
provider: string,
scopes?: string[],
): Promise<{ login_url: string; state_token: string }> {
const query = scopes ? { scopes: scopes.join(",") } : undefined;
return this._get(`/integrations/${provider}/login`, query);
}
oAuthCallback(
provider: string,
code: string,
state_token: string,
): Promise<CredentialsMetaResponse> {
return this._request("POST", `/integrations/${provider}/callback`, {
code,
state_token,
});
}
createAPIKeyCredentials(
credentials: Omit<APIKeyCredentials, "id" | "type">,
): Promise<APIKeyCredentials> {
return this._request(
"POST",
`/integrations/${credentials.provider}/credentials`,
credentials,
);
}
listCredentials(provider?: string): Promise<CredentialsMetaResponse[]> {
return this._get(
provider
? `/integrations/${provider}/credentials`
: "/integrations/credentials",
);
}
getCredentials(
provider: string,
id: string,
): Promise<APIKeyCredentials | OAuth2Credentials> {
return this._get(`/integrations/${provider}/credentials/${id}`);
}
deleteCredentials(
provider: string,
id: string,
force: boolean = true,
): Promise<
CredentialsDeleteResponse | CredentialsDeleteNeedConfirmationResponse
> {
return this._request(
"DELETE",
`/integrations/${provider}/credentials/${id}`,
force ? { force: true } : undefined,
);
}
/**
* @returns `true` if a ping event was received, `false` if provider doesn't support pinging but the webhook exists.
* @throws `Error` if the webhook does not exist.
* @throws `Error` if the attempt to ping timed out.
*/
async pingWebhook(webhook_id: string): Promise<boolean> {
return this._request("POST", `/integrations/webhooks/${webhook_id}/ping`);
}
logMetric(metric: AnalyticsMetrics) {
return this._request("POST", "/analytics/log_raw_metric", metric);
}
logAnalytic(analytic: AnalyticsDetails) {
return this._request("POST", "/analytics/log_raw_analytics", analytic);
}
///////////////////////////////////////////
/////////// V2 STORE API /////////////////
/////////////////////////////////////////
getStoreProfile(page?: string): Promise<ProfileDetails | null> {
try {
console.log("+++ Making API from: ", page);
const result = this._get("/store/profile", undefined, page);
return result;
} catch (error) {
console.error("Error fetching store profile:", error);
return Promise.resolve(null);
}
}
getStoreAgents(params?: {
featured?: boolean;
creator?: string;
sorted_by?: string;
search_query?: string;
category?: string;
page?: number;
page_size?: number;
}): Promise<StoreAgentsResponse> {
return this._get("/store/agents", params);
}
getStoreAgent(
username: string,
agentName: string,
): Promise<StoreAgentDetails> {
return this._get(`/store/agents/${username}/${agentName}`);
}
getStoreCreators(params?: {
featured?: boolean;
search_query?: string;
sorted_by?: string;
page?: number;
page_size?: number;
}): Promise<CreatorsResponse> {
return this._get("/store/creators", params);
}
getStoreCreator(username: string): Promise<CreatorDetails> {
return this._get(`/store/creator/${username}`);
}
getStoreSubmissions(params?: {
page?: number;
page_size?: number;
}): Promise<StoreSubmissionsResponse> {
return this._get("/store/submissions", params);
}
createStoreSubmission(
submission: StoreSubmissionRequest,
): Promise<StoreSubmission> {
return this._request("POST", "/store/submissions", submission);
}
deleteStoreSubmission(submission_id: string): Promise<boolean> {
return this._request("DELETE", `/store/submissions/${submission_id}`);
}
uploadStoreSubmissionMedia(file: File): Promise<string> {
const formData = new FormData();
formData.append("file", file);
return this._uploadFile("/store/submissions/media", file);
}
updateStoreProfile(profile: ProfileDetails): Promise<ProfileDetails> {
return this._request("POST", "/store/profile", profile);
}
reviewAgent(
username: string,
agentName: string,
review: StoreReviewCreate,
): Promise<StoreReview> {
console.log("Reviewing agent: ", username, agentName, review);
return this._request(
"POST",
`/store/agents/${username}/${agentName}/review`,
review,
);
}
getMyAgents(params?: {
page?: number;
page_size?: number;
}): Promise<MyAgentsResponse> {
return this._get("/store/myagents", params);
}
///////////////////////////////////////////
/////////// INTERNAL FUNCTIONS ////////////
//////////////////////////////??///////////
private async _get(path: string, query?: Record<string, any>, page?: string) {
return this._request("GET", path, query, page);
}
async createSchedule(schedule: ScheduleCreatable): Promise<Schedule> {
return this._request("POST", `/schedules`, schedule);
}
async deleteSchedule(scheduleId: string): Promise<Schedule> {
return this._request("DELETE", `/schedules/${scheduleId}`);
}
async listSchedules(): Promise<Schedule[]> {
return this._get(`/schedules`);
}
private async _uploadFile(path: string, file: File): Promise<string> {
// Get session with retry logic
let token = "no-token-found";
let retryCount = 0;
const maxRetries = 3;
while (retryCount < maxRetries) {
const {
data: { session },
} = (await this.supabaseClient?.auth.getSession()) || {
data: { session: null },
};
if (session?.access_token) {
token = session.access_token;
break;
}
retryCount++;
if (retryCount < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, 100 * retryCount));
}
}
// Create a FormData object and append the file
const formData = new FormData();
formData.append("file", file);
const response = await fetch(this.baseUrl + path, {
method: "POST",
headers: {
...(token && { Authorization: `Bearer ${token}` }),
},
body: formData,
});
if (!response.ok) {
throw new Error(`Error uploading file: ${response.statusText}`);
}
// Parse the response appropriately
const media_url = await response.text();
return media_url;
}
private async _request(
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
path: string,
payload?: Record<string, any>,
page?: string,
) {
if (method !== "GET") {
console.debug(`${method} ${path} payload:`, payload);
}
// Get session with retry logic
let token = "no-token-found";
let retryCount = 0;
const maxRetries = 3;
while (retryCount < maxRetries) {
const {
data: { session },
} = (await this.supabaseClient?.auth.getSession()) || {
data: { session: null },
};
if (session?.access_token) {
token = session.access_token;
break;
}
retryCount++;
if (retryCount < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, 100 * retryCount));
}
}
console.log("Request: ", method, path, "from: ", page);
if (token === "no-token-found") {
console.warn(
"No auth token found after retries. This may indicate a session sync issue between client and server.",
);
console.debug("Last session attempt:", retryCount);
} else {
console.log("Auth token found");
}
console.log("--------------------------------");
let url = this.baseUrl + path;
const payloadAsQuery = ["GET", "DELETE"].includes(method);
if (payloadAsQuery && payload) {
// For GET requests, use payload as query
const queryParams = new URLSearchParams(payload);
url += `?${queryParams.toString()}`;
}
const hasRequestBody = !payloadAsQuery && payload !== undefined;
const response = await fetch(url, {
method,
headers: {
...(hasRequestBody && { "Content-Type": "application/json" }),
...(token && { Authorization: `Bearer ${token}` }),
},
body: hasRequestBody ? JSON.stringify(payload) : undefined,
});
if (!response.ok) {
console.warn(`${method} ${path} returned non-OK response:`, response);
// console.warn("baseClient is attempting to redirect by changing window location")
// if (
// response.status === 403 &&
// response.statusText === "Not authenticated" &&
// typeof window !== "undefined" // Check if in browser environment
// ) {
// window.location.href = "/login";
// }
let errorDetail;
try {
const errorData = await response.json();
errorDetail = errorData.detail || response.statusText;
} catch (e) {
errorDetail = response.statusText;
}
throw new Error(errorDetail);
}
// Handle responses with no content (like DELETE requests)
if (
response.status === 204 ||
response.headers.get("Content-Length") === "0"
) {
return null;
}
try {
return await response.json();
} catch (e) {
if (e instanceof SyntaxError) {
console.warn(`${method} ${path} returned invalid JSON:`, e);
return null;
}
throw e;
}
}
startHeartbeat() {
this.stopHeartbeat();
this.heartbeatInterval = window.setInterval(() => {
if (this.webSocket?.readyState === WebSocket.OPEN) {
this.webSocket.send(
JSON.stringify({
method: "heartbeat",
data: "ping",
success: true,
}),
);
this.heartbeatTimeoutId = window.setTimeout(() => {
console.log("Heartbeat timeout - reconnecting");
this.webSocket?.close();
this.connectWebSocket();
}, this.HEARTBEAT_TIMEOUT);
}
}, this.HEARTBEAT_INTERVAL);
}
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
if (this.heartbeatTimeoutId) {
clearTimeout(this.heartbeatTimeoutId);
this.heartbeatTimeoutId = null;
}
}
handleHeartbeatResponse() {
if (this.heartbeatTimeoutId) {
clearTimeout(this.heartbeatTimeoutId);
this.heartbeatTimeoutId = null;
}
}
async connectWebSocket(): Promise<void> {
this.wsConnecting ??= new Promise(async (resolve, reject) => {
try {
const token =
(await this.supabaseClient?.auth.getSession())?.data.session
?.access_token || "";
const wsUrlWithToken = `${this.wsUrl}?token=${token}`;
this.webSocket = new WebSocket(wsUrlWithToken);
this.webSocket.onopen = () => {
console.log("WebSocket connection established");
this.startHeartbeat(); // Start heartbeat when connection opens
resolve();
};
this.webSocket.onclose = (event) => {
console.log("WebSocket connection closed", event);
this.stopHeartbeat(); // Stop heartbeat when connection closes
this.webSocket = null;
// Attempt to reconnect after a delay
setTimeout(() => this.connectWebSocket(), 1000);
};
this.webSocket.onerror = (error) => {
console.error("WebSocket error:", error);
this.stopHeartbeat(); // Stop heartbeat on error
reject(error);
};
this.webSocket.onmessage = (event) => {
const message: WebsocketMessage = JSON.parse(event.data);
// Handle heartbeat response
if (message.method === "heartbeat" && message.data === "pong") {
this.handleHeartbeatResponse();
return;
}
if (message.method === "execution_event") {
message.data = parseNodeExecutionResultTimestamps(message.data);
}
this.wsMessageHandlers[message.method]?.forEach((handler) =>
handler(message.data),
);
};
} catch (error) {
console.error("Error connecting to WebSocket:", error);
reject(error);
}
});
return this.wsConnecting;
}
disconnectWebSocket() {
this.stopHeartbeat(); // Stop heartbeat when disconnecting
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
this.webSocket.close();
}
}
sendWebSocketMessage<M extends keyof WebsocketMessageTypeMap>(
method: M,
data: WebsocketMessageTypeMap[M],
callCount = 0,
) {
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
this.webSocket.send(JSON.stringify({ method, data }));
} else {
this.connectWebSocket().then(() => {
callCount == 0
? this.sendWebSocketMessage(method, data, callCount + 1)
: setTimeout(
() => {
this.sendWebSocketMessage(method, data, callCount + 1);
},
2 ** (callCount - 1) * 1000,
);
});
}
}
onWebSocketMessage<M extends keyof WebsocketMessageTypeMap>(
method: M,
handler: (data: WebsocketMessageTypeMap[M]) => void,
): () => void {
this.wsMessageHandlers[method] ??= new Set();
this.wsMessageHandlers[method].add(handler);
// Return detacher
return () => this.wsMessageHandlers[method].delete(handler);
}
subscribeToExecution(graphId: string) {
this.sendWebSocketMessage("subscribe", { graph_id: graphId });
}
}
/* *** UTILITY TYPES *** */
type GraphCreateRequestBody = {
graph: GraphCreatable;
};
type WebsocketMessageTypeMap = {
subscribe: { graph_id: string };
execution_event: NodeExecutionResult;
heartbeat: "ping" | "pong";
};
type WebsocketMessage = {
[M in keyof WebsocketMessageTypeMap]: {
method: M;
data: WebsocketMessageTypeMap[M];
};
}[keyof WebsocketMessageTypeMap];
/* *** HELPER FUNCTIONS *** */
function parseNodeExecutionResultTimestamps(result: any): NodeExecutionResult {
return {
...result,
add_time: new Date(result.add_time),
queue_time: result.queue_time ? new Date(result.queue_time) : undefined,
start_time: result.start_time ? new Date(result.start_time) : undefined,
end_time: result.end_time ? new Date(result.end_time) : undefined,
};
}

View File

@ -1,15 +1,669 @@
import { SupabaseClient } from "@supabase/supabase-js";
import { createClient } from "../supabase/client";
import BaseAutoGPTServerAPI from "./baseClient";
import {
AnalyticsDetails,
AnalyticsMetrics,
APIKeyCredentials,
Block,
CredentialsDeleteNeedConfirmationResponse,
CredentialsDeleteResponse,
CredentialsMetaResponse,
GraphExecution,
Graph,
GraphCreatable,
GraphExecuteResponse,
GraphMeta,
GraphUpdateable,
NodeExecutionResult,
MyAgentsResponse,
OAuth2Credentials,
ProfileDetails,
User,
StoreAgentsResponse,
StoreAgentDetails,
CreatorsResponse,
CreatorDetails,
StoreSubmissionsResponse,
StoreSubmissionRequest,
StoreSubmission,
StoreReviewCreate,
StoreReview,
ScheduleCreatable,
Schedule,
} from "./types";
import { createBrowserClient } from "@supabase/ssr";
import getServerSupabase from "../supabase/getServerSupabase";
const isClient = typeof window !== "undefined";
export default class BackendAPI {
private baseUrl: string;
private wsUrl: string;
private webSocket: WebSocket | null = null;
private wsConnecting: Promise<void> | null = null;
private wsMessageHandlers: Record<string, Set<(data: any) => void>> = {};
heartbeatInterval: number | null = null;
readonly HEARTBEAT_INTERVAL = 10_0000; // 100 seconds
readonly HEARTBEAT_TIMEOUT = 10_000; // 10 seconds
heartbeatTimeoutId: number | null = null;
export class AutoGPTServerAPI extends BaseAutoGPTServerAPI {
constructor(
baseUrl: string = process.env.NEXT_PUBLIC_AGPT_SERVER_URL ||
"http://localhost:8006/api",
wsUrl: string = process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL ||
"ws://localhost:8001/ws",
supabaseClient: SupabaseClient | null = createClient(),
) {
super(baseUrl, wsUrl, supabaseClient);
this.baseUrl = baseUrl;
this.wsUrl = wsUrl;
}
private get supabaseClient(): SupabaseClient | null {
return isClient
? createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
)
: getServerSupabase();
}
async isAuthenticated(): Promise<boolean> {
if (!this.supabaseClient) return false;
const {
data: { user },
} = await this.supabaseClient?.auth.getUser();
return user != null;
}
createUser(): Promise<User> {
return this._request("POST", "/auth/user", {});
}
getUserCredit(page?: string): Promise<{ credits: number }> {
try {
return this._get(`/credits`, undefined, page);
} catch (error) {
return Promise.resolve({ credits: 0 });
}
}
getBlocks(): Promise<Block[]> {
return this._get("/blocks");
}
listGraphs(): Promise<GraphMeta[]> {
return this._get(`/graphs`);
}
getExecutions(): Promise<GraphExecution[]> {
return this._get(`/executions`);
}
getGraph(
id: string,
version?: number,
hide_credentials?: boolean,
): Promise<Graph> {
let query: Record<string, any> = {};
if (version !== undefined) {
query["version"] = version;
}
if (hide_credentials !== undefined) {
query["hide_credentials"] = hide_credentials;
}
return this._get(`/graphs/${id}`, query);
}
getGraphAllVersions(id: string): Promise<Graph[]> {
return this._get(`/graphs/${id}/versions`);
}
createGraph(graphCreateBody: GraphCreatable): Promise<Graph>;
createGraph(graphID: GraphCreatable | string): Promise<Graph> {
let requestBody = { graph: graphID } as GraphCreateRequestBody;
return this._request("POST", "/graphs", requestBody);
}
updateGraph(id: string, graph: GraphUpdateable): Promise<Graph> {
return this._request("PUT", `/graphs/${id}`, graph);
}
deleteGraph(id: string): Promise<void> {
return this._request("DELETE", `/graphs/${id}`);
}
setGraphActiveVersion(id: string, version: number): Promise<Graph> {
return this._request("PUT", `/graphs/${id}/versions/active`, {
active_graph_version: version,
});
}
executeGraph(
id: string,
inputData: { [key: string]: any } = {},
): Promise<GraphExecuteResponse> {
return this._request("POST", `/graphs/${id}/execute`, inputData);
}
async getGraphExecutionInfo(
graphID: string,
runID: string,
): Promise<NodeExecutionResult[]> {
return (await this._get(`/graphs/${graphID}/executions/${runID}`)).map(
parseNodeExecutionResultTimestamps,
);
}
async stopGraphExecution(
graphID: string,
runID: string,
): Promise<NodeExecutionResult[]> {
return (
await this._request("POST", `/graphs/${graphID}/executions/${runID}/stop`)
).map(parseNodeExecutionResultTimestamps);
}
oAuthLogin(
provider: string,
scopes?: string[],
): Promise<{ login_url: string; state_token: string }> {
const query = scopes ? { scopes: scopes.join(",") } : undefined;
return this._get(`/integrations/${provider}/login`, query);
}
oAuthCallback(
provider: string,
code: string,
state_token: string,
): Promise<CredentialsMetaResponse> {
return this._request("POST", `/integrations/${provider}/callback`, {
code,
state_token,
});
}
createAPIKeyCredentials(
credentials: Omit<APIKeyCredentials, "id" | "type">,
): Promise<APIKeyCredentials> {
return this._request(
"POST",
`/integrations/${credentials.provider}/credentials`,
credentials,
);
}
listCredentials(provider?: string): Promise<CredentialsMetaResponse[]> {
return this._get(
provider
? `/integrations/${provider}/credentials`
: "/integrations/credentials",
);
}
getCredentials(
provider: string,
id: string,
): Promise<APIKeyCredentials | OAuth2Credentials> {
return this._get(`/integrations/${provider}/credentials/${id}`);
}
deleteCredentials(
provider: string,
id: string,
force: boolean = true,
): Promise<
CredentialsDeleteResponse | CredentialsDeleteNeedConfirmationResponse
> {
return this._request(
"DELETE",
`/integrations/${provider}/credentials/${id}`,
force ? { force: true } : undefined,
);
}
/**
* @returns `true` if a ping event was received, `false` if provider doesn't support pinging but the webhook exists.
* @throws `Error` if the webhook does not exist.
* @throws `Error` if the attempt to ping timed out.
*/
async pingWebhook(webhook_id: string): Promise<boolean> {
return this._request("POST", `/integrations/webhooks/${webhook_id}/ping`);
}
logMetric(metric: AnalyticsMetrics) {
return this._request("POST", "/analytics/log_raw_metric", metric);
}
logAnalytic(analytic: AnalyticsDetails) {
return this._request("POST", "/analytics/log_raw_analytics", analytic);
}
///////////////////////////////////////////
/////////// V2 STORE API /////////////////
/////////////////////////////////////////
getStoreProfile(page?: string): Promise<ProfileDetails | null> {
try {
console.log("+++ Making API from: ", page);
const result = this._get("/store/profile", undefined, page);
return result;
} catch (error) {
console.error("Error fetching store profile:", error);
return Promise.resolve(null);
}
}
getStoreAgents(params?: {
featured?: boolean;
creator?: string;
sorted_by?: string;
search_query?: string;
category?: string;
page?: number;
page_size?: number;
}): Promise<StoreAgentsResponse> {
return this._get("/store/agents", params);
}
getStoreAgent(
username: string,
agentName: string,
): Promise<StoreAgentDetails> {
return this._get(`/store/agents/${username}/${agentName}`);
}
getStoreCreators(params?: {
featured?: boolean;
search_query?: string;
sorted_by?: string;
page?: number;
page_size?: number;
}): Promise<CreatorsResponse> {
return this._get("/store/creators", params);
}
getStoreCreator(username: string): Promise<CreatorDetails> {
return this._get(`/store/creator/${username}`);
}
getStoreSubmissions(params?: {
page?: number;
page_size?: number;
}): Promise<StoreSubmissionsResponse> {
return this._get("/store/submissions", params);
}
createStoreSubmission(
submission: StoreSubmissionRequest,
): Promise<StoreSubmission> {
return this._request("POST", "/store/submissions", submission);
}
deleteStoreSubmission(submission_id: string): Promise<boolean> {
return this._request("DELETE", `/store/submissions/${submission_id}`);
}
uploadStoreSubmissionMedia(file: File): Promise<string> {
const formData = new FormData();
formData.append("file", file);
return this._uploadFile("/store/submissions/media", file);
}
updateStoreProfile(profile: ProfileDetails): Promise<ProfileDetails> {
return this._request("POST", "/store/profile", profile);
}
reviewAgent(
username: string,
agentName: string,
review: StoreReviewCreate,
): Promise<StoreReview> {
console.log("Reviewing agent: ", username, agentName, review);
return this._request(
"POST",
`/store/agents/${username}/${agentName}/review`,
review,
);
}
getMyAgents(params?: {
page?: number;
page_size?: number;
}): Promise<MyAgentsResponse> {
return this._get("/store/myagents", params);
}
///////////////////////////////////////////
/////////// INTERNAL FUNCTIONS ////////////
//////////////////////////////??///////////
private async _get(path: string, query?: Record<string, any>, page?: string) {
return this._request("GET", path, query, page);
}
async createSchedule(schedule: ScheduleCreatable): Promise<Schedule> {
return this._request("POST", `/schedules`, schedule);
}
async deleteSchedule(scheduleId: string): Promise<Schedule> {
return this._request("DELETE", `/schedules/${scheduleId}`);
}
async listSchedules(): Promise<Schedule[]> {
return this._get(`/schedules`);
}
private async _uploadFile(path: string, file: File): Promise<string> {
// Get session with retry logic
let token = "no-token-found";
let retryCount = 0;
const maxRetries = 3;
while (retryCount < maxRetries) {
const {
data: { session },
} = (await this.supabaseClient?.auth.getSession()) || {
data: { session: null },
};
if (session?.access_token) {
token = session.access_token;
break;
}
retryCount++;
if (retryCount < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, 100 * retryCount));
}
}
// Create a FormData object and append the file
const formData = new FormData();
formData.append("file", file);
const response = await fetch(this.baseUrl + path, {
method: "POST",
headers: {
...(token && { Authorization: `Bearer ${token}` }),
},
body: formData,
});
if (!response.ok) {
throw new Error(`Error uploading file: ${response.statusText}`);
}
// Parse the response appropriately
const media_url = await response.text();
return media_url;
}
private async _request(
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
path: string,
payload?: Record<string, any>,
page?: string,
) {
if (method !== "GET") {
console.debug(`${method} ${path} payload:`, payload);
}
// Get session with retry logic
let token = "no-token-found";
let retryCount = 0;
const maxRetries = 3;
while (retryCount < maxRetries) {
const {
data: { session },
} = (await this.supabaseClient?.auth.getSession()) || {
data: { session: null },
};
if (session?.access_token) {
token = session.access_token;
break;
}
retryCount++;
if (retryCount < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, 100 * retryCount));
}
}
console.log("Request: ", method, path, "from: ", page);
if (token === "no-token-found") {
console.warn(
"No auth token found after retries. This may indicate a session sync issue between client and server.",
);
console.debug("Last session attempt:", retryCount);
} else {
console.log("Auth token found");
}
console.log("--------------------------------");
let url = this.baseUrl + path;
const payloadAsQuery = ["GET", "DELETE"].includes(method);
if (payloadAsQuery && payload) {
// For GET requests, use payload as query
const queryParams = new URLSearchParams(payload);
url += `?${queryParams.toString()}`;
}
const hasRequestBody = !payloadAsQuery && payload !== undefined;
const response = await fetch(url, {
method,
headers: {
...(hasRequestBody && { "Content-Type": "application/json" }),
...(token && { Authorization: `Bearer ${token}` }),
},
body: hasRequestBody ? JSON.stringify(payload) : undefined,
});
if (!response.ok) {
console.warn(`${method} ${path} returned non-OK response:`, response);
// console.warn("baseClient is attempting to redirect by changing window location")
// if (
// response.status === 403 &&
// response.statusText === "Not authenticated" &&
// typeof window !== "undefined" // Check if in browser environment
// ) {
// window.location.href = "/login";
// }
let errorDetail;
try {
const errorData = await response.json();
errorDetail = errorData.detail || response.statusText;
} catch (e) {
errorDetail = response.statusText;
}
throw new Error(errorDetail);
}
// Handle responses with no content (like DELETE requests)
if (
response.status === 204 ||
response.headers.get("Content-Length") === "0"
) {
return null;
}
try {
return await response.json();
} catch (e) {
if (e instanceof SyntaxError) {
console.warn(`${method} ${path} returned invalid JSON:`, e);
return null;
}
throw e;
}
}
startHeartbeat() {
this.stopHeartbeat();
this.heartbeatInterval = window.setInterval(() => {
if (this.webSocket?.readyState === WebSocket.OPEN) {
this.webSocket.send(
JSON.stringify({
method: "heartbeat",
data: "ping",
success: true,
}),
);
this.heartbeatTimeoutId = window.setTimeout(() => {
console.log("Heartbeat timeout - reconnecting");
this.webSocket?.close();
this.connectWebSocket();
}, this.HEARTBEAT_TIMEOUT);
}
}, this.HEARTBEAT_INTERVAL);
}
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
if (this.heartbeatTimeoutId) {
clearTimeout(this.heartbeatTimeoutId);
this.heartbeatTimeoutId = null;
}
}
handleHeartbeatResponse() {
if (this.heartbeatTimeoutId) {
clearTimeout(this.heartbeatTimeoutId);
this.heartbeatTimeoutId = null;
}
}
async connectWebSocket(): Promise<void> {
this.wsConnecting ??= new Promise(async (resolve, reject) => {
try {
const token =
(await this.supabaseClient?.auth.getSession())?.data.session
?.access_token || "";
const wsUrlWithToken = `${this.wsUrl}?token=${token}`;
this.webSocket = new WebSocket(wsUrlWithToken);
this.webSocket.onopen = () => {
console.log("WebSocket connection established");
this.startHeartbeat(); // Start heartbeat when connection opens
resolve();
};
this.webSocket.onclose = (event) => {
console.log("WebSocket connection closed", event);
this.stopHeartbeat(); // Stop heartbeat when connection closes
this.webSocket = null;
// Attempt to reconnect after a delay
setTimeout(() => this.connectWebSocket(), 1000);
};
this.webSocket.onerror = (error) => {
console.error("WebSocket error:", error);
this.stopHeartbeat(); // Stop heartbeat on error
reject(error);
};
this.webSocket.onmessage = (event) => {
const message: WebsocketMessage = JSON.parse(event.data);
// Handle heartbeat response
if (message.method === "heartbeat" && message.data === "pong") {
this.handleHeartbeatResponse();
return;
}
if (message.method === "execution_event") {
message.data = parseNodeExecutionResultTimestamps(message.data);
}
this.wsMessageHandlers[message.method]?.forEach((handler) =>
handler(message.data),
);
};
} catch (error) {
console.error("Error connecting to WebSocket:", error);
reject(error);
}
});
return this.wsConnecting;
}
disconnectWebSocket() {
this.stopHeartbeat(); // Stop heartbeat when disconnecting
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
this.webSocket.close();
}
}
sendWebSocketMessage<M extends keyof WebsocketMessageTypeMap>(
method: M,
data: WebsocketMessageTypeMap[M],
callCount = 0,
) {
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
this.webSocket.send(JSON.stringify({ method, data }));
} else {
this.connectWebSocket().then(() => {
callCount == 0
? this.sendWebSocketMessage(method, data, callCount + 1)
: setTimeout(
() => {
this.sendWebSocketMessage(method, data, callCount + 1);
},
2 ** (callCount - 1) * 1000,
);
});
}
}
onWebSocketMessage<M extends keyof WebsocketMessageTypeMap>(
method: M,
handler: (data: WebsocketMessageTypeMap[M]) => void,
): () => void {
this.wsMessageHandlers[method] ??= new Set();
this.wsMessageHandlers[method].add(handler);
// Return detacher
return () => this.wsMessageHandlers[method].delete(handler);
}
subscribeToExecution(graphId: string) {
this.sendWebSocketMessage("subscribe", { graph_id: graphId });
}
}
/* *** UTILITY TYPES *** */
type GraphCreateRequestBody = {
graph: GraphCreatable;
};
type WebsocketMessageTypeMap = {
subscribe: { graph_id: string };
execution_event: NodeExecutionResult;
heartbeat: "ping" | "pong";
};
type WebsocketMessage = {
[M in keyof WebsocketMessageTypeMap]: {
method: M;
data: WebsocketMessageTypeMap[M];
};
}[keyof WebsocketMessageTypeMap];
/* *** HELPER FUNCTIONS *** */
function parseNodeExecutionResultTimestamps(result: any): NodeExecutionResult {
return {
...result,
add_time: new Date(result.add_time),
queue_time: result.queue_time ? new Date(result.queue_time) : undefined,
start_time: result.start_time ? new Date(result.start_time) : undefined,
end_time: result.end_time ? new Date(result.end_time) : undefined,
};
}

View File

@ -1,16 +0,0 @@
import { createServerClient } from "../supabase/server";
import BaseAutoGPTServerAPI from "./baseClient";
export default class AutoGPTServerAPIServerSide extends BaseAutoGPTServerAPI {
private cachedToken: string | null = null;
constructor(
baseUrl: string = process.env.NEXT_PUBLIC_AGPT_SERVER_URL ||
"http://localhost:8006/api",
wsUrl: string = process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL ||
"ws://localhost:8001/ws",
supabaseClient = createServerClient(),
) {
super(baseUrl, wsUrl, supabaseClient);
}
}

View File

@ -1,14 +1,14 @@
import { AutoGPTServerAPI } from "./client";
import BackendAPI from "./client";
import React, { createContext, useMemo } from "react";
const BackendAPIProviderContext = createContext<AutoGPTServerAPI | null>(null);
const BackendAPIProviderContext = createContext<BackendAPI | null>(null);
export function BackendAPIProvider({
children,
}: {
children?: React.ReactNode;
}): React.ReactNode {
const api = useMemo(() => new AutoGPTServerAPI(), []);
const api = useMemo(() => new BackendAPI(), []);
return (
<BackendAPIProviderContext.Provider value={api}>
@ -17,7 +17,7 @@ export function BackendAPIProvider({
);
}
export function useBackendAPI(): AutoGPTServerAPI {
export function useBackendAPI(): BackendAPI {
const context = React.useContext(BackendAPIProviderContext);
if (!context) {
throw new Error(

View File

@ -1,6 +1,6 @@
import { AutoGPTServerAPI } from "./client";
import BackendAPI from "./client";
export default AutoGPTServerAPI;
export default BackendAPI;
export * from "./client";
export * from "./types";
export * from "./utils";

View File

@ -1,13 +0,0 @@
import { createBrowserClient } from "@supabase/ssr";
export function createClient() {
try {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
} catch (error) {
console.error("error creating client", error);
return null;
}
}

View File

@ -1,15 +1,12 @@
import {
createServerClient as createClient,
type CookieOptions,
} from "@supabase/ssr";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { createServerClient } from "@supabase/ssr";
export function createServerClient() {
export default function getServerSupabase() {
// Need require here, so Next.js doesn't complain about importing this on client side
const { cookies } = require("next/headers");
const cookieStore = cookies();
try {
return createClient(
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
@ -31,19 +28,9 @@ export function createServerClient() {
},
},
);
return supabase;
} catch (error) {
throw error;
}
}
export async function checkAuth() {
const supabase = createServerClient();
if (!supabase) {
console.error("No supabase client");
redirect("/login");
}
const { data, error } = await supabase.auth.getUser();
if (error || !data?.user) {
redirect("/login");
}
}

View File

@ -1,10 +1,9 @@
import { createServerClient } from "@/lib/supabase/server";
import getServerSupabase from "./getServerSupabase";
const getServerUser = async () => {
const supabase = createServerClient();
const supabase = getServerSupabase();
if (!supabase) {
console.log(">>> failed to create supabase client");
return { user: null, error: "Failed to create Supabase client" };
}

View File

@ -1,5 +1,3 @@
import { redirect } from "next/navigation";
import getServerUser from "@/hooks/getServerUser";
import React from "react";
import * as Sentry from "@sentry/nextjs";