Merge branch 'master' into aarushikansal/open-1426-server-setup-user-auth-add-user-table

This commit is contained in:
Swifty
2024-08-05 09:47:38 +02:00
committed by GitHub
92 changed files with 4817 additions and 1860 deletions

12
.github/CODEOWNERS vendored
View File

@@ -1,5 +1,7 @@
.github/workflows/ @Significant-Gravitas/devops
autogpt/ @Significant-Gravitas/maintainers
forge/ @Significant-Gravitas/forge-maintainers
benchmark/ @Significant-Gravitas/benchmark-maintainers
frontend/ @Significant-Gravitas/frontend-maintainers
* @Significant-Gravitas/maintainers
.github/workflows/ @Significant-Gravitas/devops
forge/ @Significant-Gravitas/forge-maintainers
benchmark/ @Significant-Gravitas/benchmark-maintainers
frontend/ @Significant-Gravitas/frontend-maintainers
rnd/infra @Significant-Gravitas/devops
.github/CODEOWNERS @Significant-Gravitas/admins

View File

@@ -17,6 +17,7 @@ defaults:
working-directory: rnd/autogpt_builder
jobs:
lint:
runs-on: ubuntu-latest
@@ -31,6 +32,10 @@ jobs:
run: |
npm install
- name: Check formatting with Prettier
run: |
npx prettier --check .
- name: Run lint
run: |
npm run lint

2
cli.py
View File

@@ -69,6 +69,8 @@ d88P 888 "Y88888 "Y888 "Y88P" "Y8888P88 888 888
bold=True,
)
)
else:
click.echo(click.style("🎉 Setup completed!\n", fg="green"))
@cli.group()

View File

@@ -9,7 +9,7 @@ This guide will help you setup the server and builder for the project.
<!-- The video is listed in the root Readme.md of the repo -->
We also offer this in video format. You can check it out [here](https://github.com/Significant-Gravitas/AutoGPT#how-to-get-started)
We also offer this in video format. You can check it out [here](https://github.com/Significant-Gravitas/AutoGPT#how-to-get-started).
!!! warning
**DO NOT FOLLOW ANY OUTSIDE TUTORIALS AS THEY WILL LIKELY BE OUT OF DATE**

View File

@@ -1 +1,12 @@
AGPT_SERVER_URL=http://localhost:8000/api
## Supabase credentials
## YOU ONLY NEED THEM IF YOU WANT TO USE SUPABASE USER AUTHENTICATION
## If you're using self-hosted version then you most likely don't need to set this
# NEXT_PUBLIC_SUPABASE_URL=your-project-url
# NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
## OAuth Callback URL
## This should be {domain}/auth/callback
## Only used if you're using Supabase and OAuth
AUTH_CALLBACK_URL=http://localhost:3000/auth/callback

View File

@@ -0,0 +1,4 @@
node_modules
.next
build
public

View File

@@ -1,7 +1,6 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug server-side",
"type": "node-terminal",
@@ -24,7 +23,6 @@
"uriFormat": "%s",
"action": "debugWithEdge"
}
},
}
]
}

View File

@@ -2,7 +2,19 @@ This is the frontend for AutoGPT's next generation
## Getting Started
First, run the development server:
Run the following installation once.
```bash
npm install
# or
yarn install
# or
pnpm install
# or
bun install
```
Next, run the development server:
```bash
npm run dev
@@ -18,8 +30,12 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
For subsequent runs, you do not have to `npm install` again. Simply do `npm run dev`.
If the project is updated via git, you will need to `npm install` after each update.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Deploy
## Deploy
TODO
TODO

View File

@@ -14,4 +14,4 @@
"components": "@/components",
"utils": "@/lib/utils"
}
}
}

View File

@@ -1,22 +1,22 @@
import dotenv from 'dotenv';
import dotenv from "dotenv";
// Load environment variables
dotenv.config();
/** @type {import('next').NextConfig} */
const nextConfig = {
env: {
AGPT_SERVER_URL: process.env.AGPT_SERVER_URL,
},
async redirects() {
return [
{
source: '/',
destination: '/build',
permanent: false,
},
];
},
env: {
AGPT_SERVER_URL: process.env.AGPT_SERVER_URL,
},
async redirects() {
return [
{
source: "/",
destination: "/build",
permanent: false,
},
];
},
};
export default nextConfig;

View File

@@ -6,19 +6,25 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"format": "prettier --write ."
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@supabase/ssr": "^0.4.0",
"@supabase/supabase-js": "^2.45.0",
"ajv": "^8.17.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
@@ -32,6 +38,7 @@
"react-day-picker": "^8.10.1",
"react-dom": "^18",
"react-hook-form": "^7.52.1",
"react-icons": "^5.2.1",
"react-markdown": "^9.0.1",
"react-modal": "^3.16.1",
"reactflow": "^11.11.4",
@@ -48,6 +55,7 @@
"eslint": "^8",
"eslint-config-next": "14.2.4",
"postcss": "^8",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}

View File

@@ -0,0 +1,36 @@
"use client";
import { useEffect, useState } from "react";
export default function AuthErrorPage() {
const [errorType, setErrorType] = useState<string | null>(null);
const [errorCode, setErrorCode] = useState<string | null>(null);
const [errorDescription, setErrorDescription] = useState<string | null>(null);
useEffect(() => {
// This code only runs on the client side
if (typeof window !== "undefined") {
const hash = window.location.hash.substring(1); // Remove the leading '#'
const params = new URLSearchParams(hash);
setErrorType(params.get("error"));
setErrorCode(params.get("error_code"));
setErrorDescription(
params.get("error_description")?.replace(/\+/g, " ") ?? null,
); // Replace '+' with space
}
}, []);
if (!errorType && !errorCode && !errorDescription) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Authentication Error</h1>
{errorType && <p>Error Type: {errorType}</p>}
{errorCode && <p>Error Code: {errorCode}</p>}
{errorDescription && <p>Error Description: {errorDescription}</p>}
</div>
);
}

View File

@@ -0,0 +1,36 @@
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) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
// if "next" is in param, use it as the redirect URL
const next = searchParams.get("next") ?? "/profile";
if (code) {
const supabase = createServerClient();
if (!supabase) {
return NextResponse.redirect(`${origin}/error`);
}
const { data, error } = await supabase.auth.exchangeCodeForSession(code);
// data.session?.refresh_token is available if you need to store it for later use
if (!error) {
const forwardedHost = request.headers.get("x-forwarded-host"); // original origin before load balancer
const isLocalEnv = process.env.NODE_ENV === "development";
if (isLocalEnv) {
// we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
return NextResponse.redirect(`${origin}${next}`);
} else if (forwardedHost) {
return NextResponse.redirect(`https://${forwardedHost}${next}`);
} else {
return NextResponse.redirect(`${origin}${next}`);
}
}
}
// return the user to an error page with instructions
return NextResponse.redirect(`${origin}/auth/auth-code-error`);
}

View File

@@ -0,0 +1,33 @@
import { type EmailOtpType } from "@supabase/supabase-js";
import { type NextRequest } from "next/server";
import { redirect } from "next/navigation";
import { createServerClient } from "@/lib/supabase/server";
// Email confirmation route
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const token_hash = searchParams.get("token_hash");
const type = searchParams.get("type") as EmailOtpType | null;
const next = searchParams.get("next") ?? "/";
if (token_hash && type) {
const supabase = createServerClient();
if (!supabase) {
redirect("/error");
}
const { error } = await supabase.auth.verifyOtp({
type,
token_hash,
});
if (!error) {
// redirect user to specified redirect URL or root of app
redirect(next);
}
}
// redirect the user to an error page with some instructions
redirect("/error");
}

View File

@@ -0,0 +1,3 @@
export default function ErrorPage() {
return <p>Sorry, something went wrong</p>;
}

View File

@@ -8,8 +8,6 @@
}
}
@layer base {
:root {
--background: 0 0% 100%;
@@ -67,7 +65,6 @@
}
}
@layer base {
* {
@apply border-border;

View File

@@ -1,47 +1,40 @@
import React from 'react';
import React from "react";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { Providers } from "@/app/providers";
import { NavBar } from "@/components/NavBar";
import { cn } from "@/lib/utils";
import "./globals.css";
import { Providers } from "@/app/providers";
import {NavBar} from "@/components/NavBar";
import {cn} from "@/lib/utils";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "NextGen AutoGPT",
description: "Your one stop shop to creating AI Agents",
};
export default function RootLayout({
children,
children,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={
cn(
'antialiased transition-colors',
inter.className
)
}>
return (
<html lang="en">
<body className={cn("antialiased transition-colors", inter.className)}>
<Providers
attribute="class"
defaultTheme="light"
// Feel free to remove this line if you want to use the system theme by default
// enableSystem
disableTransitionOnChange
attribute="class"
defaultTheme="light"
// Feel free to remove this line if you want to use the system theme by default
// enableSystem
disableTransitionOnChange
>
<div className="flex flex-col min-h-screen ">
<NavBar/>
<main className="flex-1 p-4 overflow-hidden">
{children}
</main>
</div>
<div className="flex flex-col min-h-screen ">
<NavBar />
<main className="flex-1 p-4 overflow-hidden">{children}</main>
</div>
</Providers>
</body>
</html>
);
</body>
</html>
);
}

View File

@@ -0,0 +1,54 @@
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { createServerClient } from "@/lib/supabase/server";
import { z } from "zod";
const loginFormSchema = z.object({
email: z.string().email().min(2).max(64),
password: z.string().min(6).max(64),
});
export async function login(values: z.infer<typeof loginFormSchema>) {
const supabase = createServerClient();
if (!supabase) {
redirect("/error");
}
// We are sure that the values are of the correct type because zod validates the form
const { data, error } = await supabase.auth.signInWithPassword(values);
if (error) {
return error.message;
}
if (data.session) {
await supabase.auth.setSession(data.session);
}
revalidatePath("/", "layout");
redirect("/profile");
}
export async function signup(values: z.infer<typeof loginFormSchema>) {
const supabase = createServerClient();
if (!supabase) {
redirect("/error");
}
// We are sure that the values are of the correct type because zod validates the form
const { data, error } = await supabase.auth.signUp(values);
if (error) {
return error.message;
}
if (data.session) {
await supabase.auth.setSession(data.session);
}
revalidatePath("/", "layout");
redirect("/profile");
}

View File

@@ -0,0 +1,206 @@
"use client";
import useUser from "@/hooks/useUser";
import { login, signup } from "./actions";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { useForm } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { PasswordInput } from "@/components/PasswordInput";
import { FaGoogle, FaGithub, FaDiscord, FaSpinner } from "react-icons/fa";
import { useState } from "react";
import { useSupabase } from "@/components/SupabaseProvider";
import { useRouter } from "next/navigation";
const loginFormSchema = z.object({
email: z.string().email().min(2).max(64),
password: z.string().min(6).max(64),
});
export default function LoginPage() {
const { supabase, isLoading: isSupabaseLoading } = useSupabase();
const { user, isLoading: isUserLoading } = useUser();
const [feedback, setFeedback] = useState<string | null>(null);
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const form = useForm<z.infer<typeof loginFormSchema>>({
resolver: zodResolver(loginFormSchema),
defaultValues: {
email: "",
password: "",
},
});
if (user) {
console.log("User exists, redirecting to profile");
router.push("/profile");
}
if (isUserLoading || isSupabaseLoading || user) {
return (
<div className="flex justify-center items-center h-[80vh]">
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
</div>
);
}
if (!supabase) {
return (
<div>
User accounts are disabled because Supabase client is unavailable
</div>
);
}
async function handleSignInWithProvider(
provider: "google" | "github" | "discord",
) {
const { data, error } = await supabase!.auth.signInWithOAuth({
provider: provider,
options: {
redirectTo:
process.env.AUTH_CALLBACK_URL ??
`http://localhost:3000/auth/callback`,
// Get Google provider_refresh_token
// queryParams: {
// access_type: 'offline',
// prompt: 'consent',
// },
},
});
if (!error) {
setFeedback(null);
return;
}
setFeedback(error.message);
}
const onLogin = async (data: z.infer<typeof loginFormSchema>) => {
setIsLoading(true);
const error = await login(data);
setIsLoading(false);
if (error) {
setFeedback(error);
return;
}
setFeedback(null);
};
const onSignup = async (data: z.infer<typeof loginFormSchema>) => {
if (await form.trigger()) {
setIsLoading(true);
const error = await signup(data);
setIsLoading(false);
if (error) {
setFeedback(error);
return;
}
setFeedback(null);
}
};
return (
<div className="flex items-center justify-center h-[80vh]">
<div className="w-full max-w-md p-8 rounded-lg shadow-md space-y-6">
<div className="mb-6 space-y-2">
<Button
className="w-full"
onClick={() => handleSignInWithProvider("google")}
variant="outline"
type="button"
disabled={isLoading}
>
<FaGoogle className="mr-2 h-4 w-4" />
Sign in with Google
</Button>
<Button
className="w-full"
onClick={() => handleSignInWithProvider("github")}
variant="outline"
type="button"
disabled={isLoading}
>
<FaGithub className="mr-2 h-4 w-4" />
Sign in with GitHub
</Button>
<Button
className="w-full"
onClick={() => handleSignInWithProvider("discord")}
variant="outline"
type="button"
disabled={isLoading}
>
<FaDiscord className="mr-2 h-4 w-4" />
Sign in with Discord
</Button>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onLogin)}>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem className="mb-4">
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="user@email.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<PasswordInput placeholder="password" {...field} />
</FormControl>
<FormDescription>
Password needs to be at least 6 characters long
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex w-full space-x-4 mt-6 mb-6">
<Button
className="w-1/2 flex justify-center"
type="submit"
disabled={isLoading}
>
Log in
</Button>
<Button
className="w-1/2 flex justify-center"
variant="outline"
type="button"
onClick={form.handleSubmit(onSignup)}
disabled={isLoading}
>
Sign up
</Button>
</div>
</form>
<p className="text-red-500 text-sm">{feedback}</p>
<p className="text-primary text-center text-sm">
By continuing you agree to everything
</p>
</Form>
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
"use client";
import { useSupabase } from "@/components/SupabaseProvider";
import { Button } from "@/components/ui/button";
import useUser from "@/hooks/useUser";
import { useRouter } from "next/navigation";
import { FaSpinner } from "react-icons/fa";
export default function PrivatePage() {
const { user, isLoading, error } = useUser();
const { supabase } = useSupabase();
const router = useRouter();
if (isLoading) {
return (
<div className="flex justify-center items-center h-[80vh]">
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
</div>
);
}
if (error || !user || !supabase) {
router.push("/login");
return null;
}
return (
<div>
<p>Hello {user.email}</p>
<Button onClick={() => supabase.auth.signOut()}>Log out</Button>
</div>
);
}

View File

@@ -1,14 +1,17 @@
'use client'
"use client";
import * as React from 'react'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { ThemeProviderProps } from 'next-themes/dist/types'
import { TooltipProvider } from '@/components/ui/tooltip'
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { ThemeProviderProps } from "next-themes/dist/types";
import { TooltipProvider } from "@/components/ui/tooltip";
import SupabaseProvider from "@/components/SupabaseProvider";
export function Providers({ children, ...props }: ThemeProviderProps) {
return (
<NextThemesProvider {...props}>
<TooltipProvider>{children}</TooltipProvider>
</NextThemesProvider>
)
}
return (
<NextThemesProvider {...props}>
<SupabaseProvider>
<TooltipProvider>{children}</TooltipProvider>
</SupabaseProvider>
</NextThemesProvider>
);
}

View File

@@ -1,9 +1,23 @@
import { BaseEdge, ConnectionLineComponentProps, getBezierPath, Position } from "reactflow";
import {
BaseEdge,
ConnectionLineComponentProps,
getBezierPath,
Position,
} from "reactflow";
const ConnectionLine: React.FC<ConnectionLineComponentProps> = ({ fromPosition, fromHandle, fromX, fromY, toPosition, toX, toY }) => {
const sourceX = fromPosition === Position.Right ?
fromX + (fromHandle?.width! / 2 - 5) : fromX - (fromHandle?.width! / 2 - 5);
const ConnectionLine: React.FC<ConnectionLineComponentProps> = ({
fromPosition,
fromHandle,
fromX,
fromY,
toPosition,
toX,
toY,
}) => {
const sourceX =
fromPosition === Position.Right
? fromX + (fromHandle?.width! / 2 - 5)
: fromX - (fromHandle?.width! / 2 - 5);
const [path] = getBezierPath({
sourceX: sourceX,
@@ -14,9 +28,7 @@ const ConnectionLine: React.FC<ConnectionLineComponentProps> = ({ fromPosition,
targetPosition: toPosition,
});
return (
<BaseEdge path={path} style={{ strokeWidth: 2, stroke: '#555' }} />
);
return <BaseEdge path={path} style={{ strokeWidth: 2, stroke: "#555" }} />;
};
export default ConnectionLine;

View File

@@ -1,20 +1,41 @@
import React, { FC, memo, useMemo, useState } from "react";
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getBezierPath, useReactFlow, XYPosition } from "reactflow";
import './customedge.css';
import { X } from 'lucide-react';
import {
BaseEdge,
EdgeLabelRenderer,
EdgeProps,
getBezierPath,
useReactFlow,
XYPosition,
} from "reactflow";
import "./customedge.css";
import { X } from "lucide-react";
export type CustomEdgeData = {
edgeColor: string
sourcePos: XYPosition
}
edgeColor: string;
sourcePos?: XYPosition;
};
const CustomEdgeFC: FC<EdgeProps<CustomEdgeData>> = ({ id, data, selected, source, sourcePosition, sourceX, sourceY, target, targetPosition, targetX, targetY, markerEnd }) => {
const CustomEdgeFC: FC<EdgeProps<CustomEdgeData>> = ({
id,
data,
selected,
source,
sourcePosition,
sourceX,
sourceY,
target,
targetPosition,
targetX,
targetY,
markerEnd,
}) => {
const [isHovered, setIsHovered] = useState(false);
const { setEdges } = useReactFlow();
const onEdgeClick = () => {
setEdges((edges) => edges.filter((edge) => edge.id !== id));
}
data.clearNodesStatusAndOutput();
};
const [path, labelX, labelY] = getBezierPath({
sourceX: sourceX - 5,
@@ -26,14 +47,29 @@ const CustomEdgeFC: FC<EdgeProps<CustomEdgeData>> = ({ id, data, selected, sourc
});
// Calculate y difference between source and source node, to adjust self-loop edge
const yDifference = useMemo(() => sourceY - data!.sourcePos.y, [data!.sourcePos.y]);
const yDifference = useMemo(
() => sourceY - (data?.sourcePos?.y || 0),
[data?.sourcePos?.y],
);
// Define special edge path for self-loop
const edgePath = source === target ?
`M ${sourceX - 5} ${sourceY} C ${sourceX + 128} ${sourceY - yDifference - 128} ${targetX - 128} ${sourceY - yDifference - 128} ${targetX + 3}, ${targetY}` :
path;
const edgePath =
source === target
? `M ${sourceX - 5} ${sourceY} C ${sourceX + 128} ${sourceY - yDifference - 128} ${targetX - 128} ${sourceY - yDifference - 128} ${targetX + 3}, ${targetY}`
: path;
console.table({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, path, labelX, labelY });
console.table({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
path,
labelX,
labelY,
});
return (
<>
@@ -42,7 +78,9 @@ const CustomEdgeFC: FC<EdgeProps<CustomEdgeData>> = ({ id, data, selected, sourc
markerEnd={markerEnd}
style={{
strokeWidth: isHovered ? 3 : 2,
stroke: (data?.edgeColor ?? '#555555') + (selected || isHovered ? '' : '80')
stroke:
(data?.edgeColor ?? "#555555") +
(selected || isHovered ? "" : "80"),
}}
/>
<path
@@ -57,16 +95,16 @@ const CustomEdgeFC: FC<EdgeProps<CustomEdgeData>> = ({ id, data, selected, sourc
<EdgeLabelRenderer>
<div
style={{
position: 'absolute',
position: "absolute",
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
pointerEvents: 'all',
pointerEvents: "all",
}}
className="edge-label-renderer"
>
<button
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={`edge-label-button ${isHovered ? 'visible' : ''}`}
className={`edge-label-button ${isHovered ? "visible" : ""}`}
onClick={onEdgeClick}
>
<X className="size-4" />
@@ -74,7 +112,7 @@ const CustomEdgeFC: FC<EdgeProps<CustomEdgeData>> = ({ id, data, selected, sourc
</div>
</EdgeLabelRenderer>
</>
)
);
};
export const CustomEdge = memo(CustomEdgeFC);

View File

@@ -1,24 +1,41 @@
import React, { useState, useEffect, FC, memo, useCallback } from 'react';
import { NodeProps, useReactFlow } from 'reactflow';
import 'reactflow/dist/style.css';
import './customnode.css';
import InputModalComponent from './InputModalComponent';
import OutputModalComponent from './OutputModalComponent';
import { BlockSchema } from '@/lib/types';
import { beautifyString, setNestedProperty } from '@/lib/utils';
import { Switch } from "@/components/ui/switch"
import NodeHandle from './NodeHandle';
import NodeInputField from './NodeInputField';
import { Copy, Trash2 } from 'lucide-react';
import React, {
useState,
useEffect,
FC,
memo,
useCallback,
useRef,
} from "react";
import { NodeProps, useReactFlow } from "reactflow";
import "reactflow/dist/style.css";
import "./customnode.css";
import InputModalComponent from "./InputModalComponent";
import OutputModalComponent from "./OutputModalComponent";
import {
BlockIORootSchema,
NodeExecutionResult,
} from "@/lib/autogpt-server-api/types";
import { BlockSchema } from "@/lib/types";
import { beautifyString, setNestedProperty } from "@/lib/utils";
import { Switch } from "@/components/ui/switch";
import NodeHandle from "./NodeHandle";
import NodeInputField from "./NodeInputField";
import { Copy, Trash2 } from "lucide-react";
import { history } from "./history";
export type CustomNodeData = {
blockType: string;
title: string;
inputSchema: BlockSchema;
outputSchema: BlockSchema;
inputSchema: BlockIORootSchema;
outputSchema: BlockIORootSchema;
hardcodedValues: { [key: string]: any };
setHardcodedValues: (values: { [key: string]: any }) => void;
connections: Array<{ source: string; sourceHandle: string; target: string; targetHandle: string }>;
connections: Array<{
source: string;
sourceHandle: string;
target: string;
targetHandle: string;
}>;
isOutputOpen: boolean;
status?: string;
output_data?: any;
@@ -34,13 +51,14 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
const [isAdvancedOpen, setIsAdvancedOpen] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [activeKey, setActiveKey] = useState<string | null>(null);
const [modalValue, setModalValue] = useState<string>('');
const [modalValue, setModalValue] = useState<string>("");
const [isOutputModalOpen, setIsOutputModalOpen] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const { getNode, setNodes, getEdges, setEdges } = useReactFlow();
const outputDataRef = useRef<HTMLDivElement>(null);
const isInitialSetup = useRef(true);
useEffect(() => {
if (data.output_data || data.status) {
@@ -48,10 +66,18 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
}
}, [data.output_data, data.status]);
useEffect(() => {
setIsOutputOpen(data.isOutputOpen);
}, [data.isOutputOpen]);
useEffect(() => {
data.setIsAnyModalOpen?.(isModalOpen || isOutputModalOpen);
}, [isModalOpen, isOutputModalOpen, data]);
useEffect(() => {
isInitialSetup.current = false;
}, []);
const toggleOutput = (checked: boolean) => {
setIsOutputOpen(checked);
};
@@ -61,23 +87,31 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
};
const hasOptionalFields = () => {
return data.inputSchema && Object.keys(data.inputSchema.properties).some((key) => {
return !(data.inputSchema.required?.includes(key));
});
return (
data.inputSchema &&
Object.keys(data.inputSchema.properties).some((key) => {
return !data.inputSchema.required?.includes(key);
})
);
};
const generateOutputHandles = (schema: BlockSchema) => {
const generateOutputHandles = (schema: BlockIORootSchema) => {
if (!schema?.properties) return null;
const keys = Object.keys(schema.properties);
return keys.map((key) => (
<div key={key}>
<NodeHandle keyName={key} isConnected={isHandleConnected(key)} schema={schema.properties[key]} side="right" />
<NodeHandle
keyName={key}
isConnected={isHandleConnected(key)}
schema={schema.properties[key]}
side="right"
/>
</div>
));
};
const handleInputChange = (key: string, value: any) => {
const keys = key.split('.');
const keys = key.split(".");
const newValues = JSON.parse(JSON.stringify(data.hardcodedValues));
let current = newValues;
@@ -88,6 +122,16 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
current[keys[keys.length - 1]] = value;
console.log(`Updating hardcoded values for node ${id}:`, newValues);
if (!isInitialSetup.current) {
history.push({
type: "UPDATE_INPUT",
payload: { nodeId: id, oldValues: data.hardcodedValues, newValues },
undo: () => data.setHardcodedValues(data.hardcodedValues),
redo: () => data.setHardcodedValues(newValues),
});
}
data.setHardcodedValues(newValues);
const errors = data.errors || {};
// Remove error with the same key
@@ -96,27 +140,39 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
};
const getValue = (key: string) => {
const keys = key.split('.');
return keys.reduce((acc, k) => (acc && acc[k] !== undefined) ? acc[k] : '', data.hardcodedValues);
const keys = key.split(".");
return keys.reduce(
(acc, k) => (acc && acc[k] !== undefined ? acc[k] : ""),
data.hardcodedValues,
);
};
const isHandleConnected = (key: string) => {
return data.connections && data.connections.some((conn: any) => {
if (typeof conn === 'string') {
const [source, target] = conn.split(' -> ');
return (target.includes(key) && target.includes(data.title)) ||
(source.includes(key) && source.includes(data.title));
}
return (conn.target === id && conn.targetHandle === key) ||
(conn.source === id && conn.sourceHandle === key);
});
return (
data.connections &&
data.connections.some((conn: any) => {
if (typeof conn === "string") {
const [source, target] = conn.split(" -> ");
return (
(target.includes(key) && target.includes(data.title)) ||
(source.includes(key) && source.includes(data.title))
);
}
return (
(conn.target === id && conn.targetHandle === key) ||
(conn.source === id && conn.sourceHandle === key)
);
})
);
};
const handleInputClick = (key: string) => {
console.log(`Opening modal for key: ${key}`);
setActiveKey(key);
const value = getValue(key);
setModalValue(typeof value === 'object' ? JSON.stringify(value, null, 2) : value);
setModalValue(
typeof value === "object" ? JSON.stringify(value, null, 2) : value,
);
setIsModalOpen(true);
};
@@ -135,74 +191,89 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
const handleOutputClick = () => {
setIsOutputModalOpen(true);
setModalValue(typeof data.output_data === 'object' ? JSON.stringify(data.output_data, null, 2) : data.output_data);
setModalValue(
data.output_data
? JSON.stringify(data.output_data, null, 2)
: "[no output (yet)]",
);
};
const isTextTruncated = (element: HTMLElement | null): boolean => {
if (!element) return false;
return element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth;
return (
element.scrollHeight > element.clientHeight ||
element.scrollWidth > element.clientWidth
);
};
const handleHovered = () => {
setIsHovered(true);
console.log('isHovered', isHovered);
}
console.log("isHovered", isHovered);
};
const handleMouseLeave = () => {
setIsHovered(false);
console.log('isHovered', isHovered);
}
console.log("isHovered", isHovered);
};
const deleteNode = useCallback(() => {
console.log('Deleting node:', id);
console.log("Deleting node:", id);
// Get all edges connected to this node
const connectedEdges = getEdges().filter(edge => edge.source === id || edge.target === id);
const connectedEdges = getEdges().filter(
(edge) => edge.source === id || edge.target === id,
);
// For each connected edge, update the connected node's state
connectedEdges.forEach(edge => {
connectedEdges.forEach((edge) => {
const connectedNodeId = edge.source === id ? edge.target : edge.source;
const connectedNode = getNode(connectedNodeId);
if (connectedNode) {
setNodes(nodes => nodes.map(node => {
if (node.id === connectedNodeId) {
// Update the node's data to reflect the disconnection
const updatedConnections = node.data.connections.filter(
conn => !(conn.source === id || conn.target === id)
);
return {
...node,
data: {
...node.data,
connections: updatedConnections
}
};
}
return node;
}));
setNodes((nodes) =>
nodes.map((node) => {
if (node.id === connectedNodeId) {
// Update the node's data to reflect the disconnection
const updatedConnections = node.data.connections.filter(
(conn) => !(conn.source === id || conn.target === id),
);
return {
...node,
data: {
...node.data,
connections: updatedConnections,
},
};
}
return node;
}),
);
}
});
// Remove the node and its connected edges
setNodes(nodes => nodes.filter(node => node.id !== id));
setEdges(edges => edges.filter(edge => edge.source !== id && edge.target !== id));
setNodes((nodes) => nodes.filter((node) => node.id !== id));
setEdges((edges) =>
edges.filter((edge) => edge.source !== id && edge.target !== id),
);
}, [id, setNodes, setEdges, getNode, getEdges]);
const copyNode = useCallback(() => {
// This is a placeholder function. The actual copy functionality
// will be implemented by another team member.
console.log('Copy node:', id);
console.log("Copy node:", id);
}, [id]);
return (
<div
className={`custom-node dark-theme ${data.status?.toLowerCase() ?? ''}`}
<div
className={`custom-node dark-theme ${data.status?.toLowerCase() ?? ""}`}
onMouseEnter={handleHovered}
onMouseLeave={handleMouseLeave}
>
>
<div className="mb-2">
<div className="text-lg font-bold">{beautifyString(data.blockType?.replace(/Block$/, '') || data.title)}</div>
<div className="text-lg font-bold">
{beautifyString(data.blockType?.replace(/Block$/, "") || data.title)}
</div>
<div className="node-actions">
{isHovered && (
<>
@@ -229,19 +300,28 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
{data.inputSchema &&
Object.entries(data.inputSchema.properties).map(([key, schema]) => {
const isRequired = data.inputSchema.required?.includes(key);
return (isRequired || isAdvancedOpen) && (
<div key={key} onMouseOver={() => { }}>
<NodeHandle keyName={key} isConnected={isHandleConnected(key)} isRequired={isRequired} schema={schema} side="left" />
{!isHandleConnected(key) &&
<NodeInputField
return (
(isRequired || isAdvancedOpen) && (
<div key={key} onMouseOver={() => {}}>
<NodeHandle
keyName={key}
isConnected={isHandleConnected(key)}
isRequired={isRequired}
schema={schema}
value={getValue(key)}
handleInputClick={handleInputClick}
handleInputChange={handleInputChange}
errors={data.errors?.[key]}
/>}
</div>
side="left"
/>
{!isHandleConnected(key) && (
<NodeInputField
keyName={key}
schema={schema}
value={getValue(key)}
handleInputClick={handleInputClick}
handleInputChange={handleInputChange}
errors={data.errors?.[key]}
/>
)}
</div>
)
);
})}
</div>
@@ -252,17 +332,20 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
{isOutputOpen && (
<div className="node-output" onClick={handleOutputClick}>
<p>
<strong>Status:</strong>{' '}
{typeof data.status === 'object' ? JSON.stringify(data.status) : data.status || 'N/A'}
<strong>Status:</strong>{" "}
{typeof data.status === "object"
? JSON.stringify(data.status)
: data.status || "N/A"}
</p>
<p>
<strong>Output Data:</strong>{' '}
<strong>Output Data:</strong>{" "}
{(() => {
const outputText = typeof data.output_data === 'object'
? JSON.stringify(data.output_data)
: data.output_data;
const outputText =
typeof data.output_data === "object"
? JSON.stringify(data.output_data)
: data.output_data;
if (!outputText) return 'No output data';
if (!outputText) return "No output data";
return outputText.length > 100
? `${outputText.slice(0, 100)}... Press To Read More`
@@ -272,12 +355,15 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
</div>
)}
<div className="flex items-center mt-2.5">
<Switch onCheckedChange={toggleOutput} className='custom-switch' />
<span className='m-1 mr-4'>Output</span>
<Switch onCheckedChange={toggleOutput} className="custom-switch" />
<span className="m-1 mr-4">Output</span>
{hasOptionalFields() && (
<>
<Switch onCheckedChange={toggleAdvancedSettings} className='custom-switch' />
<span className='m-1'>Advanced</span>
<Switch
onCheckedChange={toggleAdvancedSettings}
className="custom-switch"
/>
<span className="m-1">Advanced</span>
</>
)}
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import React, { FC, useEffect, useRef } from 'react';
import { Button } from './ui/button';
import { Textarea } from './ui/textarea';
import React, { FC, useEffect, useRef } from "react";
import { Button } from "./ui/button";
import { Textarea } from "./ui/textarea";
interface ModalProps {
isOpen: boolean;
@@ -9,7 +9,12 @@ interface ModalProps {
value: string;
}
const InputModalComponent: FC<ModalProps> = ({ isOpen, onClose, onSave, value }) => {
const InputModalComponent: FC<ModalProps> = ({
isOpen,
onClose,
onSave,
value,
}) => {
const [tempValue, setTempValue] = React.useState(value);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
@@ -34,7 +39,9 @@ const InputModalComponent: FC<ModalProps> = ({ isOpen, onClose, onSave, value })
return (
<div className="nodrag fixed inset-0 bg-white bg-opacity-60 flex justify-center items-center">
<div className="bg-white p-5 rounded-lg w-[500px] max-w-[90%]">
<center><h1>Enter input text</h1></center>
<center>
<h1>Enter input text</h1>
</center>
<Textarea
ref={textAreaRef}
className="w-full h-[200px] p-2.5 rounded border border-[#dfdfdf] text-black bg-[#dfdfdf]"

View File

@@ -1,99 +1,102 @@
import {
DropdownMenu,
DropdownMenuContent, DropdownMenuItem,
DropdownMenuTrigger
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import Link from "next/link";
import { Menu } from "lucide-react";
import { Button } from "@/components/ui/button";
import { CircleUser, Menu, SquareActivity, Workflow } from "lucide-react";
import { Button, buttonVariants } from "@/components/ui/button";
import React from "react";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { Pencil1Icon, TimerIcon } from "@radix-ui/react-icons";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import Image from "next/image";
import getServerUser from "@/hooks/getServerUser";
import ProfileDropdown from "./ProfileDropdown";
export function NavBar() {
return (
<header className="sticky top-0 flex h-16 items-center gap-4 border-b bg-background px-4 md:px-6">
<div className="flex items-center gap-4 flex-1">
<Sheet>
<SheetTrigger asChild>
<Button
variant="outline"
size="icon"
className="shrink-0 md:hidden"
>
<Menu className="size-5"/>
<span className="sr-only">Toggle navigation menu</span>
</Button>
</SheetTrigger>
<SheetContent side="left">
<nav className="grid gap-6 text-lg font-medium">
<Link
href="/monitor"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 "
>
<TimerIcon className="size-6" /> Monitor
</Link>
<Link
href="/build"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2"
>
<Pencil1Icon className="size-6"/> Build
</Link>
</nav>
</SheetContent>
</Sheet>
<nav className="hidden md:flex md:flex-row md:items-center md:gap-5 lg:gap-6">
<Link
href="/monitor"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 items-center"
>
<TimerIcon className="size-4"/> Monitor
</Link>
<Link
href="/build"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 items-center"
>
<Pencil1Icon className="size-4"/> Build
</Link>
</nav>
</div>
<div className="flex-1 flex justify-center relative">
<a
className="pointer-events-auto flex place-items-center gap-2"
href="https://news.agpt.co/"
target="_blank"
rel="noopener noreferrer"
>
By{" "}
<Image
src="/AUTOgpt_Logo_dark.png"
alt="AutoGPT Logo"
width={100}
height={20}
priority
/>
</a>
</div>
<div className="flex items-center gap-4 flex-1 justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="size-8">
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn"/>
<AvatarFallback>CN</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuItem>Switch Workspace</DropdownMenuItem>
<DropdownMenuItem>Log out</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
);
}
export async function NavBar() {
const isAvailable = Boolean(
process.env.NEXT_PUBLIC_SUPABASE_URL &&
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
);
const { user } = await getServerUser();
return (
<header className="sticky top-0 flex h-16 items-center gap-4 border-b bg-background px-4 md:px-6">
<div className="flex items-center gap-4 flex-1">
<Sheet>
<SheetTrigger asChild>
<Button
variant="outline"
size="icon"
className="shrink-0 md:hidden"
>
<Menu className="size-5" />
<span className="sr-only">Toggle navigation menu</span>
</Button>
</SheetTrigger>
<SheetContent side="left">
<nav className="grid gap-6 text-lg font-medium">
<Link
href="/monitor"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 "
>
<SquareActivity className="size-6" /> Monitor
</Link>
<Link
href="/build"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2"
>
<Workflow className="size-6" /> Build
</Link>
</nav>
</SheetContent>
</Sheet>
<nav className="hidden md:flex md:flex-row md:items-center md:gap-5 lg:gap-6">
<Link
href="/monitor"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 items-center"
>
<SquareActivity className="size-4" /> Monitor
</Link>
<Link
href="/build"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 items-center"
>
<Workflow className="size-4" /> Build
</Link>
</nav>
</div>
<div className="flex-1 flex justify-center relative">
<a
className="pointer-events-auto flex place-items-center gap-2"
href="https://news.agpt.co/"
target="_blank"
rel="noopener noreferrer"
>
By{" "}
<Image
src="/AUTOgpt_Logo_dark.png"
alt="AutoGPT Logo"
width={100}
height={20}
priority
/>
</a>
</div>
<div className="flex items-center gap-4 flex-1 justify-end">
{isAvailable && !user && (
<Link
href="/login"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 items-center"
>
Log In
<CircleUser className="size-5" />
</Link>
)}
{isAvailable && user && <ProfileDropdown />}
</div>
</header>
);
}

View File

@@ -1,51 +1,59 @@
import { BlockSchema } from "@/lib/types";
import { BlockIOSchema } from "@/lib/autogpt-server-api/types";
import { beautifyString, getTypeBgColor, getTypeTextColor } from "@/lib/utils";
import { FC } from "react";
import { Handle, Position } from "reactflow";
import SchemaTooltip from "./SchemaTooltip";
type HandleProps = {
keyName: string,
schema: BlockSchema,
isConnected: boolean,
isRequired?: boolean,
side: 'left' | 'right'
}
const NodeHandle: FC<HandleProps> = ({ keyName, schema, isConnected, isRequired, side }) => {
keyName: string;
schema: BlockIOSchema;
isConnected: boolean;
isRequired?: boolean;
side: "left" | "right";
};
const NodeHandle: FC<HandleProps> = ({
keyName,
schema,
isConnected,
isRequired,
side,
}) => {
const typeName: Record<string, string> = {
string: 'text',
number: 'number',
boolean: 'true/false',
object: 'complex',
array: 'list',
null: 'null',
string: "text",
number: "number",
boolean: "true/false",
object: "complex",
array: "list",
null: "null",
};
const typeClass = `text-sm ${getTypeTextColor(schema.type)} ${side === 'left' ? 'text-left' : 'text-right'}`;
const typeClass = `text-sm ${getTypeTextColor(schema.type)} ${side === "left" ? "text-left" : "text-right"}`;
const label = (
<div className="flex flex-col flex-grow">
<span className="text-m text-gray-900 -mb-1 green">
{schema.title || beautifyString(keyName)}{isRequired ? '*' : ''}
{schema.title || beautifyString(keyName)}
{isRequired ? "*" : ""}
</span>
<span className={typeClass}>{typeName[schema.type]}</span>
</div>
);
const dot = (
<div className={`w-4 h-4 m-1 ${isConnected ? getTypeBgColor(schema.type) : 'bg-gray-600'} rounded-full transition-colors duration-100 group-hover:bg-gray-300`} />
<div
className={`w-4 h-4 m-1 ${isConnected ? getTypeBgColor(schema.type) : "bg-gray-600"} rounded-full transition-colors duration-100 group-hover:bg-gray-300`}
/>
);
if (side === 'left') {
if (side === "left") {
return (
<div key={keyName} className="handle-container">
<Handle
type="target"
position={Position.Left}
id={keyName}
className='group -ml-[29px]'
className="group -ml-[29px]"
>
<div className="pointer-events-none flex items-center">
{dot}
@@ -54,7 +62,7 @@ const NodeHandle: FC<HandleProps> = ({ keyName, schema, isConnected, isRequired,
</Handle>
<SchemaTooltip schema={schema} />
</div>
)
);
} else {
return (
<div key={keyName} className="handle-container justify-end">
@@ -62,16 +70,16 @@ const NodeHandle: FC<HandleProps> = ({ keyName, schema, isConnected, isRequired,
type="source"
position={Position.Right}
id={keyName}
className='group -mr-[29px]'
className="group -mr-[29px]"
>
<div className="pointer-events-none flex items-center">
{label}
{dot}
</div>
</Handle>
</div >
)
</div>
);
}
}
};
export default NodeHandle;

View File

@@ -1,296 +1,357 @@
import { Cross2Icon, PlusIcon } from "@radix-ui/react-icons";
import { beautifyString } from "@/lib/utils";
import { BlockIOSchema } from "@/lib/autogpt-server-api/types";
import { FC, useState } from "react";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
type BlockInputFieldProps = {
keyName: string
schema: any
parentKey?: string
value: string | Array<string> | { [key: string]: string }
handleInputClick: (key: string) => void
handleInputChange: (key: string, value: any) => void
errors?: { [key: string]: string } | string | null
}
keyName: string;
schema: BlockIOSchema;
parentKey?: string;
value: string | Array<string> | { [key: string]: string };
handleInputClick: (key: string) => void;
handleInputChange: (key: string, value: any) => void;
errors?: { [key: string]: string } | string | null;
};
const NodeInputField: FC<BlockInputFieldProps> =
({ keyName: key, schema, parentKey = '', value, handleInputClick, handleInputChange, errors }) => {
const [newKey, setNewKey] = useState<string>('');
const [newValue, setNewValue] = useState<string>('');
const [keyValuePairs, setKeyValuePairs] = useState<{ key: string, value: string }[]>([]);
const NodeInputField: FC<BlockInputFieldProps> = ({
keyName: key,
schema,
parentKey = "",
value,
handleInputClick,
handleInputChange,
errors,
}) => {
const fullKey = parentKey ? `${parentKey}.${key}` : key;
const error = typeof errors === "string" ? errors : (errors?.[key] ?? "");
const displayKey = schema.title || beautifyString(key);
const fullKey = parentKey ? `${parentKey}.${key}` : key;
const error = typeof errors === 'string' ? errors : errors?.[key] ?? "";
const displayKey = schema.title || beautifyString(key);
const [keyValuePairs, _setKeyValuePairs] = useState<
{ key: string; value: string }[]
>(
"additionalProperties" in schema && value
? Object.entries(value).map(([key, value]) => ({
key: key,
value: value,
}))
: [],
);
const handleAddProperty = () => {
if (newKey && newValue) {
const newPairs = [...keyValuePairs, { key: newKey, value: newValue }];
setKeyValuePairs(newPairs);
setNewKey('');
setNewValue('');
const expectedFormat = newPairs.reduce((acc, pair) => ({ ...acc, [pair.key]: pair.value }), {});
handleInputChange('expected_format', expectedFormat);
}
};
function setKeyValuePairs(newKVPairs: typeof keyValuePairs): void {
_setKeyValuePairs(newKVPairs);
handleInputChange(
fullKey,
newKVPairs.reduce(
(obj, { key, value }) => ({ ...obj, [key]: value }),
{},
),
);
}
const renderClickableInput = (value: string | null = null, placeholder: string = "", secret: boolean = false) => {
const className = `clickable-input ${error ? 'border-error' : ''}`
const renderClickableInput = (
value: string | null = null,
placeholder: string = "",
secret: boolean = false,
) => {
const className = `clickable-input ${error ? "border-error" : ""}`;
// if secret is true, then the input field will be a password field if the value is not null
return secret ? (
<div className={className} onClick={() => handleInputClick(fullKey)}>
{value ? <span>********</span> : <i className="text-gray-500">{placeholder}</i>}
</div>
) : (
<div className={className} onClick={() => handleInputClick(fullKey)}>
{value || <i className="text-gray-500">{placeholder}</i>}
</div>
)
};
return secret ? (
<div className={className} onClick={() => handleInputClick(fullKey)}>
{value ? (
<span>********</span>
) : (
<i className="text-gray-500">{placeholder}</i>
)}
</div>
) : (
<div className={className} onClick={() => handleInputClick(fullKey)}>
{value || <i className="text-gray-500">{placeholder}</i>}
</div>
);
};
if (schema.type === 'object' && schema.properties) {
return (
<div key={fullKey} className="object-input">
<strong>{displayKey}:</strong>
{Object.entries(schema.properties).map(([propKey, propSchema]: [string, any]) => (
<div key={`${fullKey}.${propKey}`} className="nested-input">
<NodeInputField
keyName={propKey}
schema={propSchema}
parentKey={fullKey}
value={(value as { [key: string]: string })[propKey]}
handleInputClick={handleInputClick}
handleInputChange={handleInputChange}
errors={errors}
if ("properties" in schema) {
return (
<div key={fullKey} className="object-input">
<strong>{displayKey}:</strong>
{Object.entries(schema.properties).map(([propKey, propSchema]) => (
<div key={`${fullKey}.${propKey}`} className="nested-input">
<NodeInputField
keyName={propKey}
schema={propSchema}
parentKey={fullKey}
value={(value as { [key: string]: string })[propKey]}
handleInputClick={handleInputClick}
handleInputChange={handleInputChange}
errors={errors}
/>
</div>
))}
</div>
);
}
if (schema.type === "object" && schema.additionalProperties) {
return (
<div key={fullKey}>
<div>
{keyValuePairs.map(({ key, value }, index) => (
<div
key={index}
className="flex items-center w-[325px] space-x-2 mb-2"
>
<Input
type="text"
placeholder="Key"
value={key}
onChange={(e) =>
setKeyValuePairs(
keyValuePairs.toSpliced(index, 1, {
key: e.target.value,
value: value,
}),
)
}
/>
</div>
))}
</div>
);
}
if (schema.type === 'object' && schema.additionalProperties) {
const objectValue = value || {};
return (
<div key={fullKey} className="object-input">
<strong>{displayKey}:</strong>
{Object.entries(objectValue).map(([propKey, propValue]: [string, any]) => (
<div key={`${fullKey}.${propKey}`} className="nested-input">
<div className="clickable-input" onClick={() => handleInputClick(`${fullKey}.${propKey}`)}>
{beautifyString(propKey)}: {typeof propValue === 'object' ? JSON.stringify(propValue, null, 2) : propValue}
</div>
<Button onClick={() => handleInputChange(`${fullKey}.${propKey}`, undefined)} className="array-item-remove">
&times;
<Input
type="text"
placeholder="Value"
value={value}
onChange={(e) =>
setKeyValuePairs(
keyValuePairs.toSpliced(index, 1, {
key: key,
value: e.target.value,
}),
)
}
/>
<Button
variant="ghost"
className="px-2"
onClick={() =>
setKeyValuePairs(keyValuePairs.toSpliced(index, 1))
}
>
<Cross2Icon />
</Button>
</div>
))}
{key === 'expected_format' && (
<div className="nested-input">
{keyValuePairs.map((pair, index) => (
<div key={index} className="key-value-input">
<Input
type="text"
placeholder="Key"
value={beautifyString(pair.key)}
onChange={(e) => {
const newPairs = [...keyValuePairs];
newPairs[index].key = e.target.value;
setKeyValuePairs(newPairs);
const expectedFormat = newPairs.reduce((acc, pair) => ({ ...acc, [pair.key]: pair.value }), {});
handleInputChange('expected_format', expectedFormat);
}}
/>
<Input
type="text"
placeholder="Value"
value={beautifyString(pair.value)}
onChange={(e) => {
const newPairs = [...keyValuePairs];
newPairs[index].value = e.target.value;
setKeyValuePairs(newPairs);
const expectedFormat = newPairs.reduce((acc, pair) => ({ ...acc, [pair.key]: pair.value }), {});
handleInputChange('expected_format', expectedFormat);
}}
/>
</div>
))}
<div className="key-value-input">
<Input
type="text"
placeholder="Key"
value={newKey}
onChange={(e) => setNewKey(e.target.value)}
/>
<Input
type="text"
placeholder="Value"
value={newValue}
onChange={(e) => setNewValue(e.target.value)}
/>
</div>
<Button onClick={handleAddProperty}>Add Property</Button>
</div>
<Button
className="w-full"
onClick={() =>
setKeyValuePairs(keyValuePairs.concat({ key: "", value: "" }))
}
>
<PlusIcon className="mr-2" /> Add Property
</Button>
</div>
{error && <span className="error-message">{error}</span>}
</div>
);
}
if ("anyOf" in schema) {
const types = schema.anyOf.map((s) => ("type" in s ? s.type : undefined));
if (types.includes("string") && types.includes("null")) {
return (
<div key={fullKey} className="input-container">
{renderClickableInput(
value as string,
schema.placeholder || `Enter ${displayKey} (optional)`,
)}
{error && <span className="error-message">{error}</span>}
</div>
);
}
}
if (schema.anyOf) {
const types = schema.anyOf.map((s: any) => s.type);
if (types.includes('string') && types.includes('null')) {
return (
<div key={fullKey} className="input-container">
{renderClickableInput(value as string, schema.placeholder || `Enter ${displayKey} (optional)`)}
{error && <span className="error-message">{error}</span>}
</div>
);
}
}
if ("allOf" in schema) {
return (
<div key={fullKey} className="object-input">
<strong>{displayKey}:</strong>
{"properties" in schema.allOf[0] &&
Object.entries(schema.allOf[0].properties).map(
([propKey, propSchema]) => (
<div key={`${fullKey}.${propKey}`} className="nested-input">
<NodeInputField
keyName={propKey}
schema={propSchema}
parentKey={fullKey}
value={(value as { [key: string]: string })[propKey]}
handleInputClick={handleInputClick}
handleInputChange={handleInputChange}
errors={errors}
/>
</div>
),
)}
</div>
);
}
if (schema.allOf) {
return (
<div key={fullKey} className="object-input">
<strong>{displayKey}:</strong>
{schema.allOf[0].properties && Object.entries(schema.allOf[0].properties).map(([propKey, propSchema]: [string, any]) => (
<div key={`${fullKey}.${propKey}`} className="nested-input">
<NodeInputField
keyName={propKey}
schema={propSchema}
parentKey={fullKey}
value={(value as { [key: string]: string })[propKey]}
handleInputClick={handleInputClick}
handleInputChange={handleInputChange}
errors={errors}
/>
</div>
))}
</div>
);
}
if ("oneOf" in schema) {
return (
<div key={fullKey} className="object-input">
<strong>{displayKey}:</strong>
{"properties" in schema.oneOf[0] &&
Object.entries(schema.oneOf[0].properties).map(
([propKey, propSchema]) => (
<div key={`${fullKey}.${propKey}`} className="nested-input">
<NodeInputField
keyName={propKey}
schema={propSchema}
parentKey={fullKey}
value={(value as { [key: string]: string })[propKey]}
handleInputClick={handleInputClick}
handleInputChange={handleInputChange}
errors={errors}
/>
</div>
),
)}
</div>
);
}
if (schema.oneOf) {
return (
<div key={fullKey} className="object-input">
<strong>{displayKey}:</strong>
{schema.oneOf[0].properties && Object.entries(schema.oneOf[0].properties).map(([propKey, propSchema]: [string, any]) => (
<div key={`${fullKey}.${propKey}`} className="nested-input">
<NodeInputField
keyName={propKey}
schema={propSchema}
parentKey={fullKey}
value={(value as { [key: string]: string })[propKey]}
handleInputClick={handleInputClick}
handleInputChange={handleInputChange}
errors={errors}
/>
</div>
))}
</div>
);
}
if (!("type" in schema)) {
console.warn(`Schema for input ${key} does not specify a type:`, schema);
return (
<div key={fullKey} className="input-container">
{renderClickableInput(
value as string,
schema.placeholder || `Enter ${beautifyString(displayKey)} (Complex)`,
)}
{error && <span className="error-message">{error}</span>}
</div>
);
}
switch (schema.type) {
case 'string':
if (schema.enum) {
return (
<div key={fullKey} className="input-container">
<select
value={value as string || ''}
onChange={(e) => handleInputChange(fullKey, e.target.value)}
className="select-input"
>
<option value="">Select {displayKey}</option>
{schema.enum.map((option: string) => (
<option key={option} value={option}>
{beautifyString(option)}
</option>
))}
</select>
{error && <span className="error-message">{error}</span>}
</div>
)
}
else if (schema.secret) {
return (<div key={fullKey} className="input-container">
{renderClickableInput(value as string, schema.placeholder || `Enter ${displayKey}`, true)}
{error && <span className="error-message">{error}</span>}
</div>)
}
else {
return (
<div key={fullKey} className="input-container">
{renderClickableInput(value as string, schema.placeholder || `Enter ${displayKey}`)}
{error && <span className="error-message">{error}</span>}
</div>
);
}
case 'boolean':
switch (schema.type) {
case "string":
if (schema.enum) {
return (
<div key={fullKey} className="input-container">
<select
value={value === undefined ? '' : value.toString()}
onChange={(e) => handleInputChange(fullKey, e.target.value === 'true')}
value={(value as string) || ""}
onChange={(e) => handleInputChange(fullKey, e.target.value)}
className="select-input"
>
<option value="">Select {displayKey}</option>
<option value="true">True</option>
<option value="false">False</option>
{schema.enum.map((option: string) => (
<option key={option} value={option}>
{beautifyString(option)}
</option>
))}
</select>
{error && <span className="error-message">{error}</span>}
</div>
);
case 'number':
case 'integer':
}
if (schema.secret) {
return (
<div key={fullKey} className="input-container">
<Input
type="number"
value={value as string || ''}
onChange={(e) => handleInputChange(fullKey, parseFloat(e.target.value))}
className={`number-input ${error ? 'border-error' : ''}`}
/>
{renderClickableInput(
value as string,
schema.placeholder || `Enter ${displayKey}`,
true,
)}
{error && <span className="error-message">{error}</span>}
</div>
);
case 'array':
if (schema.items && schema.items.type === 'string') {
const arrayValues = value as Array<string> || [];
return (
<div key={fullKey} className="input-container">
{arrayValues.map((item: string, index: number) => (
<div key={`${fullKey}.${index}`} className="array-item-container">
<Input
type="text"
value={item}
onChange={(e) => handleInputChange(`${fullKey}.${index}`, e.target.value)}
className="array-item-input"
/>
<Button onClick={() => handleInputChange(`${fullKey}.${index}`, '')} className="array-item-remove">
&times;
</Button>
</div>
))}
<Button onClick={() => handleInputChange(fullKey, [...arrayValues, ''])} className="array-item-add">
Add Item
</Button>
{error && <span className="error-message ml-2">{error}</span>}
</div>
);
}
return null;
default:
}
return (
<div key={fullKey} className="input-container">
{renderClickableInput(
value as string,
schema.placeholder || `Enter ${displayKey}`,
)}
{error && <span className="error-message">{error}</span>}
</div>
);
case "boolean":
return (
<div key={fullKey} className="input-container">
<select
value={value === undefined ? "" : value.toString()}
onChange={(e) =>
handleInputChange(fullKey, e.target.value === "true")
}
className="select-input"
>
<option value="">Select {displayKey}</option>
<option value="true">True</option>
<option value="false">False</option>
</select>
{error && <span className="error-message">{error}</span>}
</div>
);
case "number":
case "integer":
return (
<div key={fullKey} className="input-container">
<Input
type="number"
value={(value as string) || ""}
onChange={(e) =>
handleInputChange(fullKey, parseFloat(e.target.value))
}
className={`number-input ${error ? "border-error" : ""}`}
/>
{error && <span className="error-message">{error}</span>}
</div>
);
case "array":
if (schema.items && schema.items.type === "string") {
const arrayValues = (value as Array<string>) || [];
return (
<div key={fullKey} className="input-container">
{renderClickableInput(value as string, schema.placeholder || `Enter ${beautifyString(displayKey)} (Complex)`)}
{error && <span className="error-message">{error}</span>}
{arrayValues.map((item: string, index: number) => (
<div key={`${fullKey}.${index}`} className="array-item-container">
<Input
type="text"
value={item}
onChange={(e) =>
handleInputChange(`${fullKey}.${index}`, e.target.value)
}
className="array-item-input"
/>
<Button
onClick={() => handleInputChange(`${fullKey}.${index}`, "")}
className="array-item-remove"
>
&times;
</Button>
</div>
))}
<Button
onClick={() => handleInputChange(fullKey, [...arrayValues, ""])}
className="array-item-add"
>
Add Item
</Button>
{error && <span className="error-message ml-2">{error}</span>}
</div>
);
}
}
return null;
default:
console.warn(`Schema for input ${key} specifies unknown type:`, schema);
return (
<div key={fullKey} className="input-container">
{renderClickableInput(
value as string,
schema.placeholder ||
`Enter ${beautifyString(displayKey)} (Complex)`,
)}
{error && <span className="error-message">{error}</span>}
</div>
);
}
};
export default NodeInputField;

View File

@@ -1,7 +1,7 @@
import React, { FC, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { Button } from './ui/button';
import { Textarea } from './ui/textarea';
import React, { FC, useEffect } from "react";
import { createPortal } from "react-dom";
import { Button } from "./ui/button";
import { Textarea } from "./ui/textarea";
interface OutputModalProps {
isOpen: boolean;
@@ -9,7 +9,11 @@ interface OutputModalProps {
value: string;
}
const OutputModalComponent: FC<OutputModalProps> = ({ isOpen, onClose, value }) => {
const OutputModalComponent: FC<OutputModalProps> = ({
isOpen,
onClose,
value,
}) => {
const [tempValue, setTempValue] = React.useState(value);
useEffect(() => {
@@ -25,7 +29,9 @@ const OutputModalComponent: FC<OutputModalProps> = ({ isOpen, onClose, value })
return createPortal(
<div className="fixed inset-0 bg-white bg-opacity-60 flex justify-center items-center z-50">
<div className="bg-white p-5 rounded-lg w-[1000px] max-w-[100%]">
<center><h1 style={{ color: 'black' }}>Full Output</h1></center>
<center>
<h1 style={{ color: "black" }}>Full Output</h1>
</center>
<Textarea
className="w-full h-[400px] p-2.5 rounded border border-[#dfdfdf] text-black bg-[#dfdfdf]"
value={tempValue}
@@ -36,7 +42,7 @@ const OutputModalComponent: FC<OutputModalProps> = ({ isOpen, onClose, value })
</div>
</div>
</div>,
document.body
document.body,
);
};

View File

@@ -0,0 +1,54 @@
import { forwardRef, useState } from "react";
import { EyeIcon, EyeOffIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input, InputProps } from "@/components/ui/input";
import { cn } from "@/lib/utils";
const PasswordInput = forwardRef<HTMLInputElement, InputProps>(
({ className, ...props }, ref) => {
const [showPassword, setShowPassword] = useState(false);
const disabled =
props.value === "" || props.value === undefined || props.disabled;
return (
<div className="relative">
<Input
type={showPassword ? "text" : "password"}
className={cn("hide-password-toggle pr-10", className)}
ref={ref}
{...props}
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute top-0 right-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowPassword((prev) => !prev)}
disabled={disabled}
>
{showPassword && !disabled ? (
<EyeIcon className="w-4 h-4" aria-hidden="true" />
) : (
<EyeOffIcon className="w-4 h-4" aria-hidden="true" />
)}
<span className="sr-only">
{showPassword ? "Hide password" : "Show password"}
</span>
</Button>
{/* hides browsers password toggles */}
<style>{`
.hide-password-toggle::-ms-reveal,
.hide-password-toggle::-ms-clear {
visibility: hidden;
pointer-events: none;
display: none;
}
`}</style>
</div>
);
},
);
PasswordInput.displayName = "PasswordInput";
export { PasswordInput };

View File

@@ -0,0 +1,40 @@
"use client";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "./ui/button";
import { useSupabase } from "./SupabaseProvider";
import { useRouter } from "next/navigation";
import useUser from "@/hooks/useUser";
const ProfileDropdown = () => {
const { supabase } = useSupabase();
const router = useRouter();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 rounded-full">
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => router.push("profile")}>
Profile
</DropdownMenuItem>
<DropdownMenuItem onClick={() => supabase?.auth.signOut()}>
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
export default ProfileDropdown;

View File

@@ -3,12 +3,12 @@ import {
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { BlockSchema } from "@/lib/types";
import { Info } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
} from "@/components/ui/tooltip";
import { BlockIOSchema } from "@/lib/autogpt-server-api/types";
import { Info } from "lucide-react";
import ReactMarkdown from "react-markdown";
const SchemaTooltip: React.FC<{ schema: BlockSchema }> = ({ schema }) => {
const SchemaTooltip: React.FC<{ schema: BlockIOSchema }> = ({ schema }) => {
if (!schema.description) return null;
return (
@@ -18,13 +18,19 @@ const SchemaTooltip: React.FC<{ schema: BlockSchema }> = ({ schema }) => {
<Info className="p-1 rounded-full hover:bg-gray-300" size={24} />
</TooltipTrigger>
<TooltipContent className="max-w-xs tooltip-content">
<ReactMarkdown components={{
a: ({ node, ...props }) => <a className="text-blue-400 underline" {...props} />,
}}>{schema.description}</ReactMarkdown>
<ReactMarkdown
components={{
a: ({ node, ...props }) => (
<a className="text-blue-400 underline" {...props} />
),
}}
>
{schema.description}
</ReactMarkdown>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
);
};
export default SchemaTooltip;

View File

@@ -0,0 +1,60 @@
"use client";
import { createClient } from "@/lib/supabase/client";
import { SupabaseClient } from "@supabase/supabase-js";
import { useRouter } from "next/navigation";
import { createContext, useContext, useEffect, useState } from "react";
type SupabaseContextType = {
supabase: SupabaseClient | null;
isLoading: boolean;
};
const Context = createContext<SupabaseContextType | undefined>(undefined);
export default function SupabaseProvider({
children,
}: {
children: React.ReactNode;
}) {
const [supabase, setSupabase] = useState<SupabaseClient | null>(null);
const [isLoading, setIsLoading] = useState(true);
const router = useRouter();
useEffect(() => {
const initializeSupabase = async () => {
setIsLoading(true);
const client = createClient();
setSupabase(client);
setIsLoading(false);
if (client) {
const {
data: { subscription },
} = client.auth.onAuthStateChange(() => {
router.refresh();
});
return () => {
subscription.unsubscribe();
};
}
};
initializeSupabase();
}, [router]);
return (
<Context.Provider value={{ supabase, isLoading }}>
{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,7 +1,7 @@
import { z } from "zod"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import React, { useState } from "react"
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useState } from "react";
import {
Form,
FormControl,
@@ -9,28 +9,30 @@ import {
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
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 { cn } from "@/lib/utils"
import { EnterIcon } from "@radix-ui/react-icons"
} from "@/components/ui/form";
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 { cn } from "@/lib/utils";
import { EnterIcon } from "@radix-ui/react-icons";
const formSchema = z.object({
agentFile: z.instanceof(File),
agentName: z.string().min(1, "Agent name is required"),
agentDescription: z.string(),
importAsTemplate: z.boolean(),
})
});
export const AgentImportForm: React.FC<React.FormHTMLAttributes<HTMLFormElement>> = (
{ className, ...props }
) => {
const [agentObject, setAgentObject] = useState<GraphCreatable | null>(null)
const api = new AutoGPTServerAPI()
export const AgentImportForm: React.FC<
React.FormHTMLAttributes<HTMLFormElement>
> = ({ className, ...props }) => {
const [agentObject, setAgentObject] = useState<GraphCreatable | null>(null);
const api = new AutoGPTServerAPI();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
@@ -39,12 +41,12 @@ export const AgentImportForm: React.FC<React.FormHTMLAttributes<HTMLFormElement>
agentDescription: "",
importAsTemplate: false,
},
})
});
function onSubmit(values: z.infer<typeof formSchema>) {
if (!agentObject) {
form.setError("root", { message: "No Agent object to save" })
return
form.setError("root", { message: "No Agent object to save" });
return;
}
const payload: GraphCreatable = {
...agentObject,
@@ -54,15 +56,20 @@ export const AgentImportForm: React.FC<React.FormHTMLAttributes<HTMLFormElement>
is_template: values.importAsTemplate,
};
(values.importAsTemplate ? api.createTemplate(payload) : api.createGraph(payload))
(values.importAsTemplate
? api.createTemplate(payload)
: api.createGraph(payload)
)
.then((response) => {
const qID = values.importAsTemplate ? "templateID" : "flowID";
window.location.href = `/build?${qID}=${response.id}`;
})
.catch(error => {
const entity_type = values.importAsTemplate ? 'template' : 'agent';
form.setError("root", { message: `Could not create ${entity_type}: ${error}` });
})
.catch((error) => {
const entity_type = values.importAsTemplate ? "template" : "agent";
form.setError("root", {
message: `Could not create ${entity_type}: ${error}`,
});
});
}
return (
@@ -85,21 +92,22 @@ export const AgentImportForm: React.FC<React.FormHTMLAttributes<HTMLFormElement>
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
field.onChange(file)
field.onChange(file);
const reader = new FileReader();
// Attach parser to file reader
reader.onload = (event) => {
try {
const obj = JSON.parse(
event.target?.result as string
event.target?.result as string,
);
if (
!["name", "description", "nodes", "links"]
.every(key => !!obj[key])
!["name", "description", "nodes", "links"].every(
(key) => !!obj[key],
)
) {
throw new Error(
"Invalid agent object in file: "
+ JSON.stringify(obj, null, 2)
"Invalid agent object in file: " +
JSON.stringify(obj, null, 2),
);
}
const agent = obj as Graph;
@@ -158,13 +166,25 @@ export const AgentImportForm: React.FC<React.FormHTMLAttributes<HTMLFormElement>
<FormLabel>Import as</FormLabel>
<FormControl>
<div className="flex space-x-2 items-center">
<span className={field.value ? "text-gray-400 dark:text-gray-600" : ""}>Agent</span>
<span
className={
field.value ? "text-gray-400 dark:text-gray-600" : ""
}
>
Agent
</span>
<Switch
disabled={field.disabled}
checked={field.value}
onCheckedChange={field.onChange}
/>
<span className={field.value ? "" : "text-gray-400 dark:text-gray-600"}>Template</span>
<span
className={
field.value ? "" : "text-gray-400 dark:text-gray-600"
}
>
Template
</span>
</div>
</FormControl>
<FormMessage />
@@ -176,5 +196,5 @@ export const AgentImportForm: React.FC<React.FormHTMLAttributes<HTMLFormElement>
</Button>
</form>
</Form>
)
}
);
};

View File

@@ -16,7 +16,9 @@
padding: 0;
color: #555;
opacity: 0;
transition: opacity 0.2s ease-in-out, background-color 0.2s ease-in-out;
transition:
opacity 0.2s ease-in-out,
background-color 0.2s ease-in-out;
}
.edge-label-button.visible {
@@ -35,4 +37,4 @@
.react-flow__edge-interaction {
cursor: pointer;
}
}

View File

@@ -0,0 +1,65 @@
import { Card, CardContent } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import React from "react";
/**
* Represents a control element for the ControlPanel Component.
* @type {Object} Control
* @property {React.ReactNode} icon - The icon of the control from lucide-react https://lucide.dev/icons/
* @property {string} label - The label of the control, to be leveraged by ToolTip.
* @property {onclick} onClick - The function to be executed when the control is clicked.
*/
export type Control = {
icon: React.ReactNode;
label: string;
onClick: () => void;
};
interface ControlPanelProps {
controls: Control[];
children?: React.ReactNode;
}
/**
* ControlPanel component displays a panel with controls as icons with the ability to take in children.
* @param {Object} ControlPanelProps - The properties of the control panel component.
* @param {Array} ControlPanelProps.controls - An array of control objects representing actions to be preformed.
* @param {Array} ControlPanelProps.children - The child components of the control panel.
* @returns The rendered control panel component.
*/
export const ControlPanel = ({ controls, children }: ControlPanelProps) => {
return (
<aside className="hidden w-14 flex-col sm:flex">
<Card>
<CardContent className="p-0">
<div className="flex flex-col items-center gap-4 px-2 sm:py-5 rounded-radius">
{controls.map((control, index) => (
<Tooltip key={index} delayDuration={500}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => control.onClick()}
>
{control.icon}
<span className="sr-only">{control.label}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="right">{control.label}</TooltipContent>
</Tooltip>
))}
<Separator />
{children}
</div>
</CardContent>
</Card>
</aside>
);
};
export default ControlPanel;

View File

@@ -0,0 +1,94 @@
import React, { useState } from "react";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { ToyBrick } from "lucide-react";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import { beautifyString } from "@/lib/utils";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Block } from "@/lib/autogpt-server-api";
import { PlusIcon } from "@radix-ui/react-icons";
interface BlocksControlProps {
blocks: Block[];
addBlock: (id: string, name: string) => void;
}
/**
* A React functional component that displays a control for managing blocks.
*
* @component
* @param {Object} BlocksControlProps - The properties for the BlocksControl component.
* @param {Block[]} BlocksControlProps.blocks - An array of blocks to be displayed and filtered.
* @param {(id: string, name: string) => void} BlocksControlProps.addBlock - A function to call when a block is added.
* @returns The rendered BlocksControl component.
*/
export const BlocksControl: React.FC<BlocksControlProps> = ({
blocks,
addBlock,
}) => {
const [searchQuery, setSearchQuery] = useState("");
const filteredBlocks = blocks.filter((block: Block) =>
block.name.toLowerCase().includes(searchQuery.toLowerCase()),
);
return (
<Popover>
<PopoverTrigger className="hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-50 dark:text-white">
<ToyBrick className="size-4" />
</PopoverTrigger>
<PopoverContent
side="right"
sideOffset={15}
align="start"
className="w-80 p-0"
>
<Card className="border-none shadow-none">
<CardHeader className="p-4">
<div className="flex flex-row justify-between items-center">
<Label htmlFor="search-blocks">Blocks</Label>
</div>
<Input
id="search-blocks"
type="text"
placeholder="Search blocks..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</CardHeader>
<CardContent className="p-1">
<ScrollArea className="h-[60vh]">
{filteredBlocks.map((block) => (
<Card key={block.id} className="m-2">
<div className="flex items-center justify-between m-3">
<div className="flex-1 min-w-0 mr-2">
<span className="font-medium truncate block">
{beautifyString(block.name)}
</span>
</div>
<div className="flex items-center gap-1 flex-shrink-0">
<Button
variant="ghost"
size="icon"
onClick={() => addBlock(block.id, block.name)}
aria-label="Add block"
>
<PlusIcon />
</Button>
</div>
</div>
</Card>
))}
</ScrollArea>
</CardContent>
</Card>
</PopoverContent>
</Popover>
);
};

View File

@@ -0,0 +1,65 @@
import { Card, CardContent } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import React from "react";
/**
* Represents a control element for the ControlPanel Component.
* @type {Object} Control
* @property {React.ReactNode} icon - The icon of the control from lucide-react https://lucide.dev/icons/
* @property {string} label - The label of the control, to be leveraged by ToolTip.
* @property {onclick} onClick - The function to be executed when the control is clicked.
*/
export type Control = {
icon: React.ReactNode;
label: string;
onClick: () => void;
};
interface ControlPanelProps {
controls: Control[];
children?: React.ReactNode;
}
/**
* ControlPanel component displays a panel with controls as icons with the ability to take in children.
* @param {Object} ControlPanelProps - The properties of the control panel component.
* @param {Array} ControlPanelProps.controls - An array of control objects representing actions to be preformed.
* @param {Array} ControlPanelProps.children - The child components of the control panel.
* @returns The rendered control panel component.
*/
export const ControlPanel = ({ controls, children }: ControlPanelProps) => {
return (
<aside className="hidden w-14 flex-col sm:flex">
<Card>
<CardContent className="p-0">
<div className="flex flex-col items-center gap-4 px-2 sm:py-5 rounded-radius">
{children}
<Separator />
{controls.map((control, index) => (
<Tooltip key={index} delayDuration={500}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => control.onClick()}
>
{control.icon}
<span className="sr-only">{control.label}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="right">{control.label}</TooltipContent>
</Tooltip>
))}
</div>
</CardContent>
</Card>
</aside>
);
};
export default ControlPanel;

View File

@@ -0,0 +1,100 @@
import React from "react";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Card, CardContent, CardFooter } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { GraphMeta } from "@/lib/autogpt-server-api";
import { Label } from "@/components/ui/label";
import { Save } from "lucide-react";
interface SaveControlProps {
agentMeta: GraphMeta | null;
onSave: (isTemplate: boolean | undefined) => void;
onNameChange: (name: string) => void;
onDescriptionChange: (description: string) => void;
}
/**
* A SaveControl component to be used within the ControlPanel. It allows the user to save the agent / template.
* @param {Object} SaveControlProps - The properties of the SaveControl component.
* @param {GraphMeta | null} SaveControlProps.agentMeta - The agent's metadata, or null if creating a new agent.
* @param {(isTemplate: boolean | undefined) => void} SaveControlProps.onSave - Function to save the agent or template.
* @param {(name: string) => void} SaveControlProps.onNameChange - Function to handle name changes.
* @param {(description: string) => void} SaveControlProps.onDescriptionChange - Function to handle description changes.
* @returns The SaveControl component.
*/
export const SaveControl = ({
agentMeta,
onSave,
onNameChange,
onDescriptionChange,
}: SaveControlProps) => {
/**
* Note for improvement:
* At the moment we are leveraging onDescriptionChange and onNameChange to handle the changes in the description and name of the agent.
* We should migrate this to be handled with form controls and a form library.
*/
// Determines if we're saving a template or an agent
let isTemplate = agentMeta?.is_template ? true : undefined;
const handleSave = () => {
onSave(isTemplate);
};
const getType = () => {
return agentMeta?.is_template ? "template" : "agent";
};
return (
<Popover>
<PopoverTrigger className="hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-50 dark:text-white">
<Save className="size-4" />
</PopoverTrigger>
<PopoverContent side="right" sideOffset={15} align="start">
<Card className="border-none shadow-none">
<CardContent className="p-4">
<div className="grid gap-3">
<Label htmlFor="name">Name</Label>
<Input
id="name"
placeholder="Enter your agent name"
className="col-span-3"
defaultValue={agentMeta?.name || ""}
onChange={(e) => onNameChange(e.target.value)}
/>
<Label htmlFor="description">Description</Label>
<Input
id="description"
placeholder="Your agent description"
className="col-span-3"
defaultValue={agentMeta?.description || ""}
onChange={(e) => onDescriptionChange(e.target.value)}
/>
</div>
</CardContent>
<CardFooter className="flex flex-col items-stretch gap-2 ">
<Button className="w-full" onClick={handleSave}>
Save {getType()}
</Button>
{!agentMeta && (
<Button
variant="secondary"
className="w-full"
onClick={() => {
isTemplate = true;
handleSave();
}}
>
Save as Template
</Button>
)}
</CardFooter>
</Card>
</PopoverContent>
</Popover>
);
};

View File

@@ -1,13 +1,13 @@
/* flow.css or index.css */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
@@ -25,7 +25,8 @@ button:hover {
background-color: #666;
}
input, textarea {
input,
textarea {
background-color: #ffffff;
color: #000000;
border: 1px solid #555;
@@ -33,10 +34,10 @@ input, textarea {
border-radius: 4px;
width: calc(100% - 18px);
box-sizing: border-box;
margin-top: 5px;
}
input::placeholder, textarea::placeholder {
input::placeholder,
textarea::placeholder {
color: #aaa;
}

View File

@@ -0,0 +1,96 @@
// history.ts
import { CustomNodeData } from "./CustomNode";
import { CustomEdgeData } from "./CustomEdge";
import { Edge } from "reactflow";
type ActionType =
| "ADD_NODE"
| "DELETE_NODE"
| "ADD_EDGE"
| "DELETE_EDGE"
| "UPDATE_NODE"
| "MOVE_NODE"
| "UPDATE_INPUT"
| "UPDATE_NODE_POSITION";
type AddNodePayload = { node: CustomNodeData };
type DeleteNodePayload = { nodeId: string };
type AddEdgePayload = { edge: Edge<CustomEdgeData> };
type DeleteEdgePayload = { edgeId: string };
type UpdateNodePayload = { nodeId: string; newData: Partial<CustomNodeData> };
type MoveNodePayload = { nodeId: string; position: { x: number; y: number } };
type UpdateInputPayload = {
nodeId: string;
oldValues: { [key: string]: any };
newValues: { [key: string]: any };
};
type UpdateNodePositionPayload = {
nodeId: string;
oldPosition: { x: number; y: number };
newPosition: { x: number; y: number };
};
type ActionPayload =
| AddNodePayload
| DeleteNodePayload
| AddEdgePayload
| DeleteEdgePayload
| UpdateNodePayload
| MoveNodePayload
| UpdateInputPayload
| UpdateNodePositionPayload;
type Action = {
type: ActionType;
payload: ActionPayload;
undo: () => void;
redo: () => void;
};
class History {
private past: Action[] = [];
private future: Action[] = [];
push(action: Action) {
this.past.push(action);
this.future = [];
}
undo() {
const action = this.past.pop();
if (action) {
action.undo();
this.future.push(action);
}
}
redo() {
const action = this.future.pop();
if (action) {
action.redo();
this.past.push(action);
}
}
canUndo(): boolean {
return this.past.length > 0;
}
canRedo(): boolean {
return this.future.length > 0;
}
clear() {
this.past = [];
this.future = [];
}
getHistoryState() {
return {
past: [...this.past],
future: [...this.future],
};
}
}
export const history = new History();

View File

@@ -1,9 +1,9 @@
"use client"
"use client";
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
@@ -13,12 +13,12 @@ const Avatar = React.forwardRef<
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
className,
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
@@ -29,8 +29,8 @@ const AvatarImage = React.forwardRef<
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
@@ -40,11 +40,11 @@ const AvatarFallback = React.forwardRef<
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-neutral-100 dark:bg-neutral-800",
className
className,
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback }
export { Avatar, AvatarImage, AvatarFallback };

View File

@@ -1,7 +1,7 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-md border border-neutral-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 dark:border-neutral-800 dark:focus:ring-neutral-300 cursor-default",
@@ -20,8 +20,8 @@ const badgeVariants = cva(
defaultVariants: {
variant: "default",
},
}
)
},
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
@@ -30,7 +30,7 @@ export interface BadgeProps
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
);
}
export { Badge, badgeVariants }
export { Badge, badgeVariants };

View File

@@ -1,24 +1,25 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-950 disabled:pointer-events-none disabled:opacity-50 dark:focus-visible:ring-gray-300",
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 dark:focus-visible:ring-neutral-300",
{
variants: {
variant: {
default:
"bg-gray-900 text-gray-50 shadow hover:bg-gray-900/90 dark:bg-gray-50 dark:text-gray-900 dark:hover:bg-gray-50/90",
"bg-neutral-900 text-neutral-50 shadow hover:bg-neutral-900/90 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50/90",
destructive:
"bg-red-500 text-gray-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-gray-50 dark:hover:bg-red-900/90",
"bg-red-500 text-neutral-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-neutral-50 dark:hover:bg-red-900/90",
outline:
"border border-gray-200 bg-white shadow-sm hover:bg-gray-100 hover:text-gray-900 dark:border-gray-800 dark:bg-gray-950 dark:hover:bg-gray-800 dark:hover:text-gray-50",
"border border-neutral-200 bg-white shadow-sm hover:bg-neutral-100 hover:text-neutral-900 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50",
secondary:
"bg-gray-100 text-gray-900 shadow-sm hover:bg-gray-100/80 dark:bg-gray-800 dark:text-gray-50 dark:hover:bg-gray-800/80",
ghost: "hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-50 dark:text-white",
link: "text-gray-900 underline-offset-4 hover:underline dark:text-gray-50",
"bg-neutral-100 text-neutral-900 shadow-sm hover:bg-neutral-100/80 dark:bg-neutral-800 dark:text-neutral-50 dark:hover:bg-neutral-800/80",
ghost:
"hover:bg-neutral-100 hover:text-neutral-900 dark:hover:bg-neutral-800 dark:hover:text-neutral-50",
link: "text-neutral-900 underline-offset-4 hover:underline dark:text-neutral-50",
},
size: {
default: "h-9 px-4 py-2",
@@ -31,27 +32,27 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
}
)
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants }
export { Button, buttonVariants };

View File

@@ -1,13 +1,13 @@
"use client"
"use client";
import * as React from "react"
import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"
import { DayPicker } from "react-day-picker"
import * as React from "react";
import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons";
import { DayPicker } from "react-day-picker";
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
export type CalendarProps = React.ComponentProps<typeof DayPicker>
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({
className,
@@ -27,7 +27,7 @@ function Calendar({
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
@@ -40,17 +40,18 @@ function Calendar({
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-neutral-100 [&:has([aria-selected].day-outside)]:bg-neutral-100/50 [&:has([aria-selected].day-range-end)]:rounded-r-md dark:[&:has([aria-selected])]:bg-neutral-800 dark:[&:has([aria-selected].day-outside)]:bg-neutral-800/50",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md"
: "[&:has([aria-selected])]:rounded-md",
),
day: cn(
buttonVariants({ variant: "ghost" }),
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
"h-8 w-8 p-0 font-normal aria-selected:opacity-100",
),
day_range_start: "day-range-start",
day_range_end: "day-range-end",
day_selected:
"bg-neutral-900 text-neutral-50 hover:bg-neutral-900 hover:text-neutral-50 focus:bg-neutral-900 focus:text-neutral-50 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50 dark:hover:text-neutral-900 dark:focus:bg-neutral-50 dark:focus:text-neutral-900",
day_today: "bg-neutral-100 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-50",
day_today:
"bg-neutral-100 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-50",
day_outside:
"day-outside text-neutral-500 opacity-50 aria-selected:bg-neutral-100/50 aria-selected:text-neutral-500 aria-selected:opacity-30 dark:text-neutral-400 dark:aria-selected:bg-neutral-800/50 dark:aria-selected:text-neutral-400",
day_disabled: "text-neutral-500 opacity-50 dark:text-neutral-400",
@@ -65,8 +66,8 @@ function Calendar({
}}
{...props}
/>
)
);
}
Calendar.displayName = "Calendar"
Calendar.displayName = "Calendar";
export { Calendar }
export { Calendar };

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
@@ -10,12 +10,12 @@ const Card = React.forwardRef<
ref={ref}
className={cn(
"rounded-xl border border-neutral-200 bg-white text-neutral-950 shadow dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
className
className,
)}
{...props}
/>
))
Card.displayName = "Card"
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
@@ -26,8 +26,8 @@ const CardHeader = React.forwardRef<
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
@@ -38,8 +38,8 @@ const CardTitle = React.forwardRef<
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
@@ -50,16 +50,16 @@ const CardDescription = React.forwardRef<
className={cn("text-sm text-neutral-500 dark:text-neutral-400", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
@@ -70,7 +70,14 @@ const CardFooter = React.forwardRef<
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
));
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};

View File

@@ -0,0 +1,11 @@
"use client";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View File

@@ -1,18 +1,18 @@
"use client"
"use client";
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
@@ -22,12 +22,12 @@ const DialogOverlay = React.forwardRef<
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
className,
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
@@ -39,7 +39,7 @@ const DialogContent = React.forwardRef<
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-neutral-200 bg-white p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg dark:border-neutral-800 dark:bg-neutral-950",
className
className,
)}
{...props}
>
@@ -50,8 +50,8 @@ const DialogContent = React.forwardRef<
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
@@ -60,12 +60,12 @@ const DialogHeader = ({
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
className,
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
@@ -74,12 +74,12 @@ const DialogFooter = ({
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
className,
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
@@ -89,12 +89,12 @@ const DialogTitle = React.forwardRef<
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
className,
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
@@ -105,8 +105,8 @@ const DialogDescription = React.forwardRef<
className={cn("text-sm text-neutral-500 dark:text-neutral-400", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
@@ -119,4 +119,4 @@ export {
DialogFooter,
DialogTitle,
DialogDescription,
}
};

View File

@@ -1,31 +1,31 @@
"use client"
"use client";
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons"
} from "@radix-ui/react-icons";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
@@ -33,16 +33,16 @@ const DropdownMenuSubTrigger = React.forwardRef<
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-neutral-100 data-[state=open]:bg-neutral-100 dark:focus:bg-neutral-800 dark:data-[state=open]:bg-neutral-800",
inset && "pl-8",
className
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@@ -52,13 +52,13 @@ const DropdownMenuSubContent = React.forwardRef<
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-neutral-200 bg-white p-1 text-neutral-950 shadow-lg 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
className,
)}
{...props}
/>
))
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@@ -71,18 +71,18 @@ const DropdownMenuContent = React.forwardRef<
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-neutral-200 bg-white p-1 text-neutral-950 shadow-md dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
"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",
className
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
@@ -90,12 +90,12 @@ const DropdownMenuItem = React.forwardRef<
className={cn(
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
inset && "pl-8",
className
className,
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
@@ -105,7 +105,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
className
className,
)}
checked={checked}
{...props}
@@ -117,9 +117,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@@ -129,7 +129,7 @@ const DropdownMenuRadioItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-neutral-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50",
className
className,
)}
{...props}
>
@@ -140,13 +140,13 @@ const DropdownMenuRadioItem = React.forwardRef<
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
@@ -154,12 +154,12 @@ const DropdownMenuLabel = React.forwardRef<
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
className,
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
@@ -167,11 +167,14 @@ const DropdownMenuSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-neutral-100 dark:bg-neutral-800", className)}
className={cn(
"-mx-1 my-1 h-px bg-neutral-100 dark:bg-neutral-800",
className,
)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
@@ -182,9 +185,9 @@ const DropdownMenuShortcut = ({
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
@@ -202,4 +205,4 @@ export {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
};

View File

@@ -1,8 +1,8 @@
"use client"
"use client";
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import {
Controller,
ControllerProps,
@@ -10,27 +10,27 @@ import {
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
} from "react-hook-form";
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label";
const Form = FormProvider
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
{} as FormFieldContextValue,
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
@@ -38,21 +38,21 @@ const FormField = <
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState)
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext
const { id } = itemContext;
return {
id,
@@ -61,36 +61,36 @@ const useFormField = () => {
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
};
};
type FormItemContextValue = {
id: string
}
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
{} as FormItemContextValue,
);
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
);
});
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
const { error, formItemId } = useFormField();
return (
<Label
@@ -99,15 +99,16 @@ const FormLabel = React.forwardRef<
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
);
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
@@ -121,50 +122,56 @@ const FormControl = React.forwardRef<
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
);
});
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
const { formDescriptionId } = useFormField();
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-[0.8rem] text-neutral-500 dark:text-neutral-400", className)}
className={cn(
"text-[0.8rem] text-neutral-500 dark:text-neutral-400",
className,
)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
);
});
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null
return null;
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-[0.8rem] font-medium text-red-500 dark:text-red-900", className)}
className={cn(
"text-[0.8rem] font-medium text-red-500 dark:text-red-900",
className,
)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
);
});
FormMessage.displayName = "FormMessage";
export {
useFormField,
@@ -175,4 +182,4 @@ export {
FormDescription,
FormMessage,
FormField,
}
};

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
@@ -12,15 +12,15 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-gray-200 bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-950 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-800 dark:placeholder:text-gray-400 dark:focus-visible:ring-gray-300",
type == "file" ? "pt-1.5 pb-0.5" : "", // fix alignment
className
type == "file" ? "pt-1.5 pb-0.5" : "", // fix alignment
className,
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
);
},
);
Input.displayName = "Input";
export { Input }
export { Input };

View File

@@ -1,14 +1,14 @@
"use client"
"use client";
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
@@ -20,7 +20,7 @@ const Label = React.forwardRef<
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label }
export { Label };

View File

@@ -1,15 +1,15 @@
"use client"
"use client";
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
@@ -22,12 +22,12 @@ const PopoverContent = React.forwardRef<
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",
className
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

View File

@@ -0,0 +1,48 @@
"use client";
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils";
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-neutral-200 dark:bg-neutral-800" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };

View File

@@ -0,0 +1,31 @@
"use client";
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref,
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-neutral-200 dark:bg-neutral-800",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className,
)}
{...props}
/>
),
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };

View File

@@ -1,9 +1,9 @@
"use client"
"use client";
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
@@ -12,18 +12,18 @@ const Switch = React.forwardRef<
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=unchecked]:bg-neutral-200 dark:focus-visible:ring-neutral-300 dark:focus-visible:ring-offset-neutral-950 dark:data-[state=checked]:bg-neutral-50 dark:data-[state=unchecked]:bg-neutral-800",
className
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0 dark:bg-neutral-950"
"pointer-events-none block h-4 w-4 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0 dark:bg-neutral-950",
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch }
export { Switch };

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Table = React.forwardRef<
HTMLTableElement,
@@ -13,16 +13,16 @@ const Table = React.forwardRef<
{...props}
/>
</div>
))
Table.displayName = "Table"
));
Table.displayName = "Table";
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
));
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<
HTMLTableSectionElement,
@@ -33,8 +33,8 @@ const TableBody = React.forwardRef<
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
));
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
@@ -44,12 +44,12 @@ const TableFooter = React.forwardRef<
ref={ref}
className={cn(
"border-t bg-neutral-100/50 font-medium [&>tr]:last:border-b-0 dark:bg-neutral-800/50",
className
className,
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
));
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<
HTMLTableRowElement,
@@ -59,12 +59,12 @@ const TableRow = React.forwardRef<
ref={ref}
className={cn(
"border-b transition-colors hover:bg-neutral-100/50 data-[state=selected]:bg-neutral-100 dark:hover:bg-neutral-800/50 dark:data-[state=selected]:bg-neutral-800",
className
className,
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
));
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<
HTMLTableCellElement,
@@ -74,12 +74,12 @@ const TableHead = React.forwardRef<
ref={ref}
className={cn(
"h-10 px-2 text-left align-middle font-medium text-neutral-500 [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] dark:text-neutral-400",
className
className,
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
));
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<
HTMLTableCellElement,
@@ -89,12 +89,12 @@ const TableCell = React.forwardRef<
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
className,
)}
{...props}
/>
))
TableCell.displayName = "TableCell"
));
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
@@ -102,11 +102,14 @@ const TableCaption = React.forwardRef<
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-neutral-500 dark:text-neutral-400", className)}
className={cn(
"mt-4 text-sm text-neutral-500 dark:text-neutral-400",
className,
)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
));
TableCaption.displayName = "TableCaption";
export {
Table,
@@ -117,4 +120,4 @@ export {
TableRow,
TableCell,
TableCaption,
}
};

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
@@ -11,14 +11,14 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
className,
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
);
},
);
Textarea.displayName = "Textarea";
export { Textarea }
export { Textarea };

View File

@@ -1,11 +1,11 @@
"use client"
"use client";
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = ({ children, delayDuration = 10 }) => (
<TooltipPrimitive.Root delayDuration={delayDuration}>
@@ -13,7 +13,7 @@ const Tooltip = ({ children, delayDuration = 10 }) => (
</TooltipPrimitive.Root>
);
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
@@ -24,11 +24,11 @@ const TooltipContent = React.forwardRef<
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-neutral-900 px-3 py-1.5 text-xs text-neutral-50 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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:bg-neutral-50 dark:text-neutral-900",
className
className,
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@@ -0,0 +1,21 @@
import { createServerClient } from "@/lib/supabase/server";
const getServerUser = async () => {
const supabase = createServerClient();
if (!supabase) {
return { user: null, error: "Failed to create Supabase client" };
}
try {
const { data, error } = await supabase.auth.getUser();
if (error) {
return { user: null, error: error.message };
}
return { user: data.user, error: null };
} catch (error) {
return { user: null, error: (error as Error).message };
}
};
export default getServerUser;

View File

@@ -0,0 +1,53 @@
"use client";
import { useEffect, useState } from "react";
import { User, Session } from "@supabase/supabase-js";
import { useSupabase } from "@/components/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);
useEffect(() => {
if (isSupabaseLoading || !supabase) {
return;
}
const fetchUser = async () => {
try {
setIsLoading(true);
const {
data: { user },
} = await supabase.auth.getUser();
const {
data: { session },
} = await supabase.auth.getSession();
setUser(user);
setSession(session);
} catch (e) {
setError("Failed to fetch user data");
} finally {
setIsLoading(false);
}
};
fetchUser();
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
setUser(session?.user ?? null);
setIsLoading(false);
});
return () => subscription.unsubscribe();
}, [supabase, isSupabaseLoading]);
return { user, session, isLoading: isLoading || isSupabaseLoading, error };
};
export default useUser;

View File

@@ -6,7 +6,7 @@ import {
GraphMeta,
GraphExecuteResponse,
NodeExecutionResult,
} from "./types"
} from "./types";
export default class AutoGPTServerAPI {
private baseUrl: string;
@@ -15,7 +15,8 @@ export default class AutoGPTServerAPI {
private messageHandlers: { [key: string]: (data: any) => void } = {};
constructor(
baseUrl: string = process.env.AGPT_SERVER_URL || "http://localhost:8000/api"
baseUrl: string = process.env.AGPT_SERVER_URL ||
"http://localhost:8000/api",
) {
this.baseUrl = baseUrl;
this.wsUrl = `ws://${new URL(this.baseUrl).host}/ws`;
@@ -26,11 +27,11 @@ export default class AutoGPTServerAPI {
}
async listGraphs(): Promise<GraphMeta[]> {
return this._get("/graphs")
return this._get("/graphs");
}
async listTemplates(): Promise<GraphMeta[]> {
return this._get("/templates")
return this._get("/templates");
}
async getGraph(id: string, version?: number): Promise<Graph> {
@@ -52,22 +53,26 @@ export default class AutoGPTServerAPI {
}
async createGraph(graphCreateBody: GraphCreatable): Promise<Graph>;
async createGraph(fromTemplateID: string, templateVersion: number): Promise<Graph>;
async createGraph(
graphOrTemplateID: GraphCreatable | string, templateVersion?: number
fromTemplateID: string,
templateVersion: number,
): Promise<Graph>;
async createGraph(
graphOrTemplateID: GraphCreatable | string,
templateVersion?: number,
): Promise<Graph> {
let requestBody: GraphCreateRequestBody;
if (typeof(graphOrTemplateID) == "string") {
if (typeof graphOrTemplateID == "string") {
if (templateVersion == undefined) {
throw new Error("templateVersion not specified")
throw new Error("templateVersion not specified");
}
requestBody = {
template_id: graphOrTemplateID,
template_version: templateVersion,
}
};
} else {
requestBody = { graph: graphOrTemplateID }
requestBody = { graph: graphOrTemplateID };
}
return this._request("POST", "/graphs", requestBody);
@@ -87,31 +92,40 @@ export default class AutoGPTServerAPI {
}
async setGraphActiveVersion(id: string, version: number): Promise<Graph> {
return this._request(
"PUT", `/graphs/${id}/versions/active`, { active_graph_version: version }
);
return this._request("PUT", `/graphs/${id}/versions/active`, {
active_graph_version: version,
});
}
async executeGraph(
id: string, inputData: { [key: string]: any } = {}
id: string,
inputData: { [key: string]: any } = {},
): Promise<GraphExecuteResponse> {
return this._request("POST", `/graphs/${id}/execute`, inputData);
}
async listGraphRunIDs(graphID: string, graphVersion?: number): Promise<string[]> {
const query = graphVersion !== undefined ? `?graph_version=${graphVersion}` : "";
async listGraphRunIDs(
graphID: string,
graphVersion?: number,
): Promise<string[]> {
const query =
graphVersion !== undefined ? `?graph_version=${graphVersion}` : "";
return this._get(`/graphs/${graphID}/executions` + query);
}
async getGraphExecutionInfo(graphID: string, runID: string): Promise<NodeExecutionResult[]> {
return (await this._get(`/graphs/${graphID}/executions/${runID}`))
.map((result: any) => ({
...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,
}));
async getGraphExecutionInfo(
graphID: string,
runID: string,
): Promise<NodeExecutionResult[]> {
return (await this._get(`/graphs/${graphID}/executions/${runID}`)).map(
(result: any) => ({
...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,
}),
);
}
private async _get(path: string) {
@@ -129,19 +143,23 @@ export default class AutoGPTServerAPI {
const response = await fetch(
this.baseUrl + path,
method != "GET" ? {
method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
} : undefined
method != "GET"
? {
method,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
}
: undefined,
);
const response_data = await response.json();
if (!response.ok) {
console.warn(
`${method} ${path} returned non-OK response:`, response_data.detail, response
`${method} ${path} returned non-OK response:`,
response_data.detail,
response,
);
throw new Error(`HTTP error ${response.status}! ${response_data.detail}`);
}
@@ -153,17 +171,17 @@ export default class AutoGPTServerAPI {
this.socket = new WebSocket(this.wsUrl);
this.socket.onopen = () => {
console.log('WebSocket connection established');
console.log("WebSocket connection established");
resolve();
};
this.socket.onclose = (event) => {
console.log('WebSocket connection closed', event);
console.log("WebSocket connection closed", event);
this.socket = null;
};
this.socket.onerror = (error) => {
console.error('WebSocket error:', error);
console.error("WebSocket error:", error);
reject(error);
};
@@ -183,42 +201,48 @@ export default class AutoGPTServerAPI {
}
sendWebSocketMessage<M extends keyof WebsocketMessageTypeMap>(
method: M, data: WebsocketMessageTypeMap[M]
method: M,
data: WebsocketMessageTypeMap[M],
) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify({ method, data }));
} else {
console.error('WebSocket is not connected');
console.error("WebSocket is not connected");
}
}
onWebSocketMessage<M extends keyof WebsocketMessageTypeMap>(
method: M, handler: (data: WebsocketMessageTypeMap[M]) => void
method: M,
handler: (data: WebsocketMessageTypeMap[M]) => void,
) {
this.messageHandlers[method] = handler;
}
subscribeToExecution(graphId: string) {
this.sendWebSocketMessage('subscribe', { graph_id: graphId });
this.sendWebSocketMessage("subscribe", { graph_id: graphId });
}
runGraph(graphId: string, data: WebsocketMessageTypeMap["run_graph"]["data"] = {}) {
this.sendWebSocketMessage('run_graph', { graph_id: graphId, data });
runGraph(
graphId: string,
data: WebsocketMessageTypeMap["run_graph"]["data"] = {},
) {
this.sendWebSocketMessage("run_graph", { graph_id: graphId, data });
}
}
/* *** UTILITY TYPES *** */
type GraphCreateRequestBody = {
template_id: string;
template_version: number;
} | {
graph: GraphCreatable;
}
type GraphCreateRequestBody =
| {
template_id: string;
template_version: number;
}
| {
graph: GraphCreatable;
};
type WebsocketMessageTypeMap = {
subscribe: { graph_id: string; };
run_graph: { graph_id: string; data: { [key: string]: any }; };
subscribe: { graph_id: string };
run_graph: { graph_id: string; data: { [key: string]: any } };
execution_event: NodeExecutionResult;
}
};

View File

@@ -3,26 +3,76 @@ export type Block = {
id: string;
name: string;
description: string;
inputSchema: ObjectSchema;
outputSchema: ObjectSchema;
inputSchema: BlockIORootSchema;
outputSchema: BlockIORootSchema;
};
export type ObjectSchema = {
type: string;
properties: { [key: string]: any };
additionalProperties?: { type: string };
export type BlockIORootSchema = {
type: "object";
properties: { [key: string]: BlockIOSchema };
required?: string[];
additionalProperties?: { type: string };
};
export type BlockIOSchema = {
title?: string;
description?: string;
placeholder?: string;
} & (BlockIOSimpleTypeSchema | BlockIOCombinedTypeSchema);
type BlockIOSimpleTypeSchema =
| {
type: "object";
properties: { [key: string]: BlockIOSchema };
required?: string[];
additionalProperties?: { type: string };
}
| {
type: "array";
items?: BlockIOSimpleTypeSchema;
}
| {
type: "string";
enum?: string[];
secret?: true;
default?: string;
}
| {
type: "integer" | "number";
default?: number;
}
| {
type: "boolean";
default?: boolean;
}
| {
type: "null";
};
// At the time of writing, combined schemas only occur on the first nested level in a
// block schema. It is typed this way to make the use of these objects less tedious.
type BlockIOCombinedTypeSchema =
| {
allOf: [BlockIOSimpleTypeSchema];
}
| {
anyOf: BlockIOSimpleTypeSchema[];
default?: string | number | boolean | null;
}
| {
oneOf: BlockIOSimpleTypeSchema[];
default?: string | number | boolean | null;
};
/* Mirror of autogpt_server/data/graph.py:Node */
export type Node = {
id: string;
block_id: string;
input_default: { [key: string]: any };
input_nodes: Array<{ name: string, node_id: string }>;
output_nodes: Array<{ name: string, node_id: string }>;
input_nodes: Array<{ name: string; node_id: string }>;
output_nodes: Array<{ name: string; node_id: string }>;
metadata: {
position: { x: number; y: number; };
position: { x: number; y: number };
[key: string]: any;
};
};
@@ -34,11 +84,11 @@ export type Link = {
sink_id: string;
source_name: string;
sink_name: string;
}
};
export type LinkCreatable = Omit<Link, "id"> & {
id?: string;
}
};
/* Mirror of autogpt_server/data/graph.py:GraphMeta */
export type GraphMeta = {
@@ -48,7 +98,7 @@ export type GraphMeta = {
is_template: boolean;
name: string;
description: string;
}
};
/* Mirror of autogpt_server/data/graph.py:Graph */
export type Graph = GraphMeta & {
@@ -64,16 +114,16 @@ export type GraphUpdateable = Omit<
is_active?: boolean;
is_template?: boolean;
links: Array<LinkCreatable>;
}
};
export type GraphCreatable = Omit<GraphUpdateable, "id"> & { id?: string }
export type GraphCreatable = Omit<GraphUpdateable, "id"> & { id?: string };
/* Derived from autogpt_server/executor/manager.py:ExecutionManager.add_execution */
export type GraphExecuteResponse = {
/** ID of the initiated run */
id: string;
/** List of node executions */
executions: Array<{ id: string, node_id: string }>;
executions: Array<{ id: string; node_id: string }>;
};
/* Mirror of autogpt_server/data/execution.py:ExecutionResult */
@@ -83,7 +133,7 @@ export type NodeExecutionResult = {
graph_id: string;
graph_version: number;
node_id: string;
status: 'INCOMPLETE' | 'QUEUED' | 'RUNNING' | 'COMPLETED' | 'FAILED';
status: "INCOMPLETE" | "QUEUED" | "RUNNING" | "COMPLETED" | "FAILED";
input_data: { [key: string]: any };
output_data: { [key: string]: Array<any> };
add_time: Date;

View File

@@ -4,17 +4,17 @@ import { Graph, Block, Node } from "./types";
export function safeCopyGraph(graph: Graph, block_defs: Block[]): Graph {
return {
...graph,
nodes: graph.nodes.map(node => {
const block = block_defs.find(b => b.id == node.block_id)!;
nodes: graph.nodes.map((node) => {
const block = block_defs.find((b) => b.id == node.block_id)!;
return {
...node,
input_default: Object.keys(node.input_default)
.filter(k => !block.inputSchema.properties[k].secret)
.reduce((obj: Node['input_default'], key) => {
.filter((k) => !block.inputSchema.properties[k].secret)
.reduce((obj: Node["input_default"], key) => {
obj[key] = node.input_default[key];
return obj;
}, {}),
}
};
}),
}
};
}

View File

@@ -0,0 +1,12 @@
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) {
return null;
}
}

View File

@@ -0,0 +1,78 @@
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({
request,
});
const isAvailable = Boolean(
process.env.NEXT_PUBLIC_SUPABASE_URL &&
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
);
if (!isAvailable) {
return supabaseResponse;
}
try {
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
request.cookies.set(name, value),
);
supabaseResponse = NextResponse.next({
request,
});
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options),
);
},
},
},
);
// IMPORTANT: Avoid writing any logic between createServerClient and
// supabase.auth.getUser(). A simple mistake could make it very hard to debug
// issues with users being randomly logged out.
const {
data: { user },
} = await supabase.auth.getUser();
if (
!user &&
!request.nextUrl.pathname.startsWith("/login") &&
!request.nextUrl.pathname.startsWith("/auth")
) {
// no user, potentially respond by redirecting the user to the login page
const url = request.nextUrl.clone();
url.pathname = "/login";
// return NextResponse.redirect(url)
}
// IMPORTANT: You *must* return the supabaseResponse object as it is. If you're
// creating a new response object with NextResponse.next() make sure to:
// 1. Pass the request in it, like so:
// const myNewResponse = NextResponse.next({ request })
// 2. Copy over the cookies, like so:
// myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
// 3. Change the myNewResponse object to fit your needs, but avoid changing
// the cookies!
// 4. Finally:
// return myNewResponse
// If this is not done, you may be causing the browser and server to go out
// of sync and terminate the user's session prematurely!
} catch (error) {
console.error("Failed to run Supabase middleware", error);
}
return supabaseResponse;
}

View File

@@ -0,0 +1,36 @@
import {
createServerClient as createClient,
type CookieOptions,
} from "@supabase/ssr";
import { cookies } from "next/headers";
export function createServerClient() {
const cookieStore = cookies();
try {
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options),
);
} catch {
// The `setAll` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
},
);
} catch (error) {
return null;
}
}

View File

@@ -1,14 +0,0 @@
export type BlockSchema = {
type: string;
properties: { [key: string]: any };
required?: string[];
enum?: string[];
items?: BlockSchema;
additionalProperties?: { type: string };
title?: string;
description?: string;
placeholder?: string;
allOf?: any[];
anyOf?: any[];
oneOf?: any[];
};

View File

@@ -1,17 +1,18 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}
/** Derived from https://stackoverflow.com/a/7616484 */
export function hashString(str: string): number {
let hash = 0, chr: number;
let hash = 0,
chr: number;
if (str.length === 0) return hash;
for (let i = 0; i < str.length; i++) {
chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash = (hash << 5) - hash + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
@@ -19,84 +20,93 @@ export function hashString(str: string): number {
/** Derived from https://stackoverflow.com/a/32922084 */
export function deepEquals(x: any, y: any): boolean {
const ok = Object.keys, tx = typeof x, ty = typeof y;
return x && y && tx === ty && (
tx === 'object'
? (
ok(x).length === ok(y).length &&
ok(x).every(key => deepEquals(x[key], y[key]))
)
: (x === y)
const ok = Object.keys,
tx = typeof x,
ty = typeof y;
return (
x &&
y &&
tx === ty &&
(tx === "object"
? ok(x).length === ok(y).length &&
ok(x).every((key) => deepEquals(x[key], y[key]))
: x === y)
);
}
/** Get tailwind text color class from type name */
export function getTypeTextColor(type: string | null): string {
if (type === null) return 'bg-gray-500';
return {
string: 'text-green-500',
number: 'text-blue-500',
boolean: 'text-yellow-500',
object: 'text-purple-500',
array: 'text-indigo-500',
null: 'text-gray-500',
'': 'text-gray-500',
}[type] || 'text-gray-500';
if (type === null) return "bg-gray-500";
return (
{
string: "text-green-500",
number: "text-blue-500",
boolean: "text-yellow-500",
object: "text-purple-500",
array: "text-indigo-500",
null: "text-gray-500",
"": "text-gray-500",
}[type] || "text-gray-500"
);
}
/** Get tailwind bg color class from type name */
export function getTypeBgColor(type: string | null): string {
if (type === null) return 'bg-gray-500';
return {
string: 'bg-green-500',
number: 'bg-blue-500',
boolean: 'bg-yellow-500',
object: 'bg-purple-500',
array: 'bg-indigo-500',
null: 'bg-gray-500',
'': 'bg-gray-500',
}[type] || 'bg-gray-500';
if (type === null) return "bg-gray-500";
return (
{
string: "bg-green-500",
number: "bg-blue-500",
boolean: "bg-yellow-500",
object: "bg-purple-500",
array: "bg-indigo-500",
null: "bg-gray-500",
"": "bg-gray-500",
}[type] || "bg-gray-500"
);
}
export function getTypeColor(type: string | null): string {
if (type === null) return 'bg-gray-500';
return {
string: '#22c55e',
number: '#3b82f6',
boolean: '#eab308',
object: '#a855f7',
array: '#6366f1',
null: '#6b7280',
'': '#6b7280',
}[type] || '#6b7280';
if (type === null) return "bg-gray-500";
return (
{
string: "#22c55e",
number: "#3b82f6",
boolean: "#eab308",
object: "#a855f7",
array: "#6366f1",
null: "#6b7280",
"": "#6b7280",
}[type] || "#6b7280"
);
}
export function beautifyString(name: string): string {
// Regular expression to identify places to split, considering acronyms
const result = name
.replace(/([a-z])([A-Z])/g, '$1 $2') // Add space before capital letters
.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2') // Add space between acronyms and next word
.replace(/_/g, ' ') // Replace underscores with spaces
.replace(/\b\w/g, char => char.toUpperCase()); // Capitalize the first letter of each word
.replace(/([a-z])([A-Z])/g, "$1 $2") // Add space before capital letters
.replace(/([A-Z])([A-Z][a-z])/g, "$1 $2") // Add space between acronyms and next word
.replace(/_/g, " ") // Replace underscores with spaces
.replace(/\b\w/g, (char) => char.toUpperCase()); // Capitalize the first letter of each word
return applyExceptions(result);
};
}
const exceptionMap: Record<string, string> = {
'Auto GPT': 'AutoGPT',
'Gpt': 'GPT',
'Creds': 'Credentials',
'Id': 'ID',
'Openai': 'OpenAI',
'Api': 'API',
'Url': 'URL',
'Http': 'HTTP',
'Json': 'JSON',
"Auto GPT": "AutoGPT",
Gpt: "GPT",
Creds: "Credentials",
Id: "ID",
Openai: "OpenAI",
Api: "API",
Url: "URL",
Http: "HTTP",
Json: "JSON",
};
const applyExceptions = (str: string): string => {
Object.keys(exceptionMap).forEach(key => {
const regex = new RegExp(`\\b${key}\\b`, 'g');
Object.keys(exceptionMap).forEach((key) => {
const regex = new RegExp(`\\b${key}\\b`, "g");
str = str.replace(regex, exceptionMap[key]);
});
return str;
@@ -105,11 +115,11 @@ const applyExceptions = (str: string): string => {
export function exportAsJSONFile(obj: object, filename: string): void {
// Create downloadable blob
const jsonString = JSON.stringify(obj, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const blob = new Blob([jsonString], { type: "application/json" });
const url = URL.createObjectURL(blob);
// Trigger the browser to download the blob to a file
const link = document.createElement('a');
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
@@ -126,7 +136,7 @@ export function setNestedProperty(obj: any, path: string, value: any) {
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!current[key] || typeof current[key] !== 'object') {
if (!current[key] || typeof current[key] !== "object") {
current[key] = {};
}
current = current[key];
@@ -139,14 +149,20 @@ export function removeEmptyStringsAndNulls(obj: any): any {
if (Array.isArray(obj)) {
// If obj is an array, recursively remove empty strings and nulls from its elements
return obj
.map(item => removeEmptyStringsAndNulls(item))
.filter(item => item !== null && (typeof item !== 'string' || item.trim() !== ''));
} else if (typeof obj === 'object' && obj !== null) {
.map((item) => removeEmptyStringsAndNulls(item))
.filter(
(item) =>
item !== null && (typeof item !== "string" || item.trim() !== ""),
);
} else if (typeof obj === "object" && obj !== null) {
// If obj is an object, recursively remove empty strings and nulls from its properties
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (value === null || (typeof value === 'string' && value.trim() === '')) {
if (
value === null ||
(typeof value === "string" && value.trim() === "")
) {
delete obj[key];
} else {
obj[key] = removeEmptyStringsAndNulls(value);

View File

@@ -0,0 +1,19 @@
import { updateSession } from "@/lib/supabase/middleware";
import { type NextRequest } from "next/server";
export async function middleware(request: NextRequest) {
return await updateSession(request);
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* Feel free to modify this pattern to include more paths.
*/
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};

View File

@@ -2,9 +2,7 @@ import type { Config } from "tailwindcss";
const config = {
darkMode: ["class"],
content: [
'./src/**/*.{ts,tsx}',
],
content: ["./src/**/*.{ts,tsx}"],
prefix: "",
theme: {
container: {
@@ -16,64 +14,64 @@ const config = {
},
extend: {
fontFamily: {
sans: ['var(--font-geist-sans)'],
mono: ['var(--font-geist-mono)']
sans: ["var(--font-geist-sans)"],
mono: ["var(--font-geist-mono)"],
},
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
}
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' }
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0' }
}
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out'
}
}
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config;

View File

@@ -1,7 +1,7 @@
module.exports = {
devServer: {
proxy: {
'/graphs': 'http://localhost:8000'
}
}
};
"/graphs": "http://localhost:8000",
},
},
};

View File

@@ -224,6 +224,11 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@radix-ui/number@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.1.0.tgz#1e95610461a09cdf8bb05c152e76ca1278d5da46"
integrity sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==
"@radix-ui/primitive@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2"
@@ -246,6 +251,20 @@
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-collapsible@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.0.tgz#4d49ddcc7b7d38f6c82f1fd29674f6fab5353e77"
integrity sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==
dependencies:
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-presence" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-collection@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.0.tgz#f18af78e46454a2360d103c2251773028b7724ed"
@@ -447,6 +466,28 @@
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-scroll-area@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.1.0.tgz#50b24b0fc9ada151d176395bcf47b2ec68feada5"
integrity sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg==
dependencies:
"@radix-ui/number" "1.1.0"
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-direction" "1.1.0"
"@radix-ui/react-presence" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-separator@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.1.0.tgz#ee0f4d86003b0e3ea7bc6ccab01ea0adee32663e"
integrity sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==
dependencies:
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-slot@1.1.0", "@radix-ui/react-slot@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84"
@@ -606,11 +647,82 @@
classcat "^5.0.3"
zustand "^4.4.1"
"@rollup/rollup-linux-x64-gnu@^4.9.5":
version "4.19.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.1.tgz#0af2b6541ab0f4954d2c4f96bcdc7947420dd28c"
integrity sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==
"@rushstack/eslint-patch@^1.3.3":
version "1.10.3"
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz#391d528054f758f81e53210f1a1eebcf1a8b1d20"
integrity sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==
"@supabase/auth-js@2.64.4":
version "2.64.4"
resolved "https://registry.yarnpkg.com/@supabase/auth-js/-/auth-js-2.64.4.tgz#f27fdabf1ebd1b532ceb57e8bbe66969ee09cfba"
integrity sha512-9ITagy4WP4FLl+mke1rchapOH0RQpf++DI+WSG2sO1OFOZ0rW3cwAM0nCrMOxu+Zw4vJ4zObc08uvQrXx590Tg==
dependencies:
"@supabase/node-fetch" "^2.6.14"
"@supabase/functions-js@2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@supabase/functions-js/-/functions-js-2.4.1.tgz#373e75f8d3453bacd71fb64f88d7a341d7b53ad7"
integrity sha512-8sZ2ibwHlf+WkHDUZJUXqqmPvWQ3UHN0W30behOJngVh/qHHekhJLCFbh0AjkE9/FqqXtf9eoVvmYgfCLk5tNA==
dependencies:
"@supabase/node-fetch" "^2.6.14"
"@supabase/node-fetch@2.6.15", "@supabase/node-fetch@^2.6.14":
version "2.6.15"
resolved "https://registry.yarnpkg.com/@supabase/node-fetch/-/node-fetch-2.6.15.tgz#731271430e276983191930816303c44159e7226c"
integrity sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==
dependencies:
whatwg-url "^5.0.0"
"@supabase/postgrest-js@1.15.8":
version "1.15.8"
resolved "https://registry.yarnpkg.com/@supabase/postgrest-js/-/postgrest-js-1.15.8.tgz#827aaa408cdbc89e67d0a758e7a545ac86e34312"
integrity sha512-YunjXpoQjQ0a0/7vGAvGZA2dlMABXFdVI/8TuVKtlePxyT71sl6ERl6ay1fmIeZcqxiuFQuZw/LXUuStUG9bbg==
dependencies:
"@supabase/node-fetch" "^2.6.14"
"@supabase/realtime-js@2.10.2":
version "2.10.2"
resolved "https://registry.yarnpkg.com/@supabase/realtime-js/-/realtime-js-2.10.2.tgz#c2b42d17d723d2d2a9146cfad61dc3df1ce3127e"
integrity sha512-qyCQaNg90HmJstsvr2aJNxK2zgoKh9ZZA8oqb7UT2LCh3mj9zpa3Iwu167AuyNxsxrUE8eEJ2yH6wLCij4EApA==
dependencies:
"@supabase/node-fetch" "^2.6.14"
"@types/phoenix" "^1.5.4"
"@types/ws" "^8.5.10"
ws "^8.14.2"
"@supabase/ssr@^0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@supabase/ssr/-/ssr-0.4.0.tgz#3ecb607e5346e6e09d50c106c2335db0e97903dd"
integrity sha512-6WS3NUvHDhCPAFN2kJ79AQDO8+M9fJ7y2fYpxgZqIuJEpnnGsHDNnB5Xnv8CiaJIuRU+0pKboy62RVZBMfZ0Lg==
dependencies:
cookie "^0.6.0"
optionalDependencies:
"@rollup/rollup-linux-x64-gnu" "^4.9.5"
"@supabase/storage-js@2.6.0":
version "2.6.0"
resolved "https://registry.yarnpkg.com/@supabase/storage-js/-/storage-js-2.6.0.tgz#0fa5e04db760ed7f78e4394844a6d409e537adc5"
integrity sha512-REAxr7myf+3utMkI2oOmZ6sdplMZZ71/2NEIEMBZHL9Fkmm3/JnaOZVSRqvG4LStYj2v5WhCruCzuMn6oD/Drw==
dependencies:
"@supabase/node-fetch" "^2.6.14"
"@supabase/supabase-js@^2.45.0":
version "2.45.0"
resolved "https://registry.yarnpkg.com/@supabase/supabase-js/-/supabase-js-2.45.0.tgz#d0778ab1a5a3ba2f4207e6c87cc6e288786ca0e5"
integrity sha512-j66Mfs8RhzCQCKxKogAFQYH9oNhRmgIdKk6pexguI2Oc7hi+nL9UNJug5aL1tKnBdaBM3h65riPLQSdL6sWa3Q==
dependencies:
"@supabase/auth-js" "2.64.4"
"@supabase/functions-js" "2.4.1"
"@supabase/node-fetch" "2.6.15"
"@supabase/postgrest-js" "1.15.8"
"@supabase/realtime-js" "2.10.2"
"@supabase/storage-js" "2.6.0"
"@swc/counter@^0.1.3":
version "0.1.3"
resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9"
@@ -882,6 +994,13 @@
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433"
integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==
"@types/node@*":
version "22.0.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.0.0.tgz#04862a2a71e62264426083abe1e27e87cac05a30"
integrity sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==
dependencies:
undici-types "~6.11.1"
"@types/node@^20":
version "20.14.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.9.tgz#12e8e765ab27f8c421a1820c99f5f313a933b420"
@@ -889,6 +1008,11 @@
dependencies:
undici-types "~5.26.4"
"@types/phoenix@^1.5.4":
version "1.6.5"
resolved "https://registry.yarnpkg.com/@types/phoenix/-/phoenix-1.6.5.tgz#5654e14ec7ad25334a157a20015996b6d7d2075e"
integrity sha512-xegpDuR+z0UqG9fwHqNoy3rI7JDlvaPh2TY47Fl80oq6g+hXT+c/LEuE43X48clZ6lOfANl5WrPur9fYO1RJ/w==
"@types/prop-types@*":
version "15.7.12"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6"
@@ -926,6 +1050,13 @@
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc"
integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==
"@types/ws@^8.5.10":
version "8.5.12"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e"
integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==
dependencies:
"@types/node" "*"
"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.2.0.tgz#44356312aea8852a3a82deebdacd52ba614ec07a"
@@ -1366,6 +1497,11 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
cookie@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
cross-spawn@^7.0.0, cross-spawn@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -3504,6 +3640,11 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prettier@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105"
integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==
prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
@@ -3546,6 +3687,11 @@ react-hook-form@^7.52.1:
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.52.1.tgz#ec2c96437b977f8b89ae2d541a70736c66284852"
integrity sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==
react-icons@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.2.1.tgz#28c2040917b2a2eda639b0f797bff1888e018e4a"
integrity sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==
react-is@^16.10.2, react-is@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@@ -4131,6 +4277,11 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
trim-lines@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338"
@@ -4242,6 +4393,11 @@ undici-types@~5.26.4:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
undici-types@~6.11.1:
version "6.11.1"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.11.1.tgz#432ea6e8efd54a48569705a699e62d8f4981b197"
integrity sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==
unified@^11.0.0:
version "11.0.5"
resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.5.tgz#f66677610a5c0a9ee90cab2b8d4d66037026d9e1"
@@ -4377,6 +4533,19 @@ warning@^4.0.3:
dependencies:
loose-envify "^1.0.0"
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
which-boxed-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
@@ -4462,6 +4631,11 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
ws@^8.14.2:
version "8.18.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
yaml@^2.3.4:
version "2.4.5"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.5.tgz#60630b206dd6d84df97003d33fc1ddf6296cca5e"

View File

@@ -8,3 +8,6 @@ REDDIT_CLIENT_ID=
REDDIT_CLIENT_SECRET=
REDDIT_USERNAME=
REDDIT_PASSWORD=
# Discord
DISCORD_BOT_TOKEN=

View File

@@ -53,7 +53,7 @@ We use the Poetry to manage the dependencies. To set up the project, follow thes
```sh
poetry run prisma migrate dev
```
```
## Running The Server

View File

@@ -20,7 +20,19 @@ for module in modules:
# Load all Block instances from the available modules
AVAILABLE_BLOCKS = {}
for cls in Block.__subclasses__():
def all_subclasses(clz):
subclasses = clz.__subclasses__()
for subclass in subclasses:
subclasses += all_subclasses(subclass)
return subclasses
for cls in all_subclasses(Block):
if not cls.__name__.endswith("Block"):
continue
block = cls()
if not isinstance(block.id, str) or len(block.id) != 36:

View File

@@ -1,8 +1,11 @@
from typing import Any
from abc import ABC, abstractmethod
from typing import Any, Generic, List, TypeVar
from pydantic import Field
from autogpt_server.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from autogpt_server.data.model import SchemaField
from autogpt_server.util.mock import MockObject
class ValueBlock(Block):
@@ -86,28 +89,39 @@ class PrintingBlock(Block):
yield "status", "printed"
class ObjectLookupBlock(Block):
class Input(BlockSchema):
input: Any = Field(description="Dictionary to lookup from")
key: str | int = Field(description="Key to lookup in the dictionary")
T = TypeVar("T")
class Output(BlockSchema):
output: Any = Field(description="Value found for the given key")
missing: Any = Field(description="Value of the input that missing the key")
def __init__(self):
class ObjectLookupBaseInput(BlockSchema, Generic[T]):
input: T = Field(description="Dictionary to lookup from")
key: str | int = Field(description="Key to lookup in the dictionary")
class ObjectLookupBaseOutput(BlockSchema, Generic[T]):
output: T = Field(description="Value found for the given key")
missing: T = Field(description="Value of the input that missing the key")
class ObjectLookupBase(Block, ABC, Generic[T]):
@abstractmethod
def block_id(self) -> str:
pass
def __init__(self, *args, **kwargs):
input_schema = ObjectLookupBaseInput[T]
output_schema = ObjectLookupBaseOutput[T]
super().__init__(
id="b2g2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6",
id=self.block_id(),
description="Lookup the given key in the input dictionary/object/list and return the value.",
categories={BlockCategory.BASIC},
input_schema=ObjectLookupBlock.Input,
output_schema=ObjectLookupBlock.Output,
input_schema=input_schema,
output_schema=output_schema,
test_input=[
{"input": {"apple": 1, "banana": 2, "cherry": 3}, "key": "banana"},
{"input": {"x": 10, "y": 20, "z": 30}, "key": "w"},
{"input": [1, 2, 3], "key": 1},
{"input": [1, 2, 3], "key": 3},
{"input": ObjectLookupBlock.Input(input="!!", key="key"), "key": "key"},
{"input": MockObject(value="!!", key="key"), "key": "key"},
{"input": [{"k1": "v1"}, {"k2": "v2"}, {"k1": "v3"}], "key": "k1"},
],
test_output=[
@@ -118,9 +132,11 @@ class ObjectLookupBlock(Block):
("output", "key"),
("output", ["v1", "v3"]),
],
*args,
**kwargs,
)
def run(self, input_data: Input) -> BlockOutput:
def run(self, input_data: ObjectLookupBaseInput[T]) -> BlockOutput:
obj = input_data.input
key = input_data.key
@@ -139,3 +155,165 @@ class ObjectLookupBlock(Block):
yield "output", getattr(obj, key)
else:
yield "missing", input_data.input
class ObjectLookupBlock(ObjectLookupBase[Any]):
def __init__(self):
super().__init__(categories={BlockCategory.BASIC})
def block_id(self) -> str:
return "b2g2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6"
class InputBlock(ObjectLookupBase[Any]):
def __init__(self):
super().__init__(categories={BlockCategory.BASIC, BlockCategory.INPUT_OUTPUT})
def block_id(self) -> str:
return "c0a8e994-ebf1-4a9c-a4d8-89d09c86741b"
class OutputBlock(ObjectLookupBase[Any]):
def __init__(self):
super().__init__(categories={BlockCategory.BASIC, BlockCategory.INPUT_OUTPUT})
def block_id(self) -> str:
return "363ae599-353e-4804-937e-b2ee3cef3da4"
class DictionaryAddEntryBlock(Block):
class Input(BlockSchema):
dictionary: dict | None = SchemaField(
default=None,
description="The dictionary to add the entry to. If not provided, a new dictionary will be created.",
placeholder='{"key1": "value1", "key2": "value2"}',
)
key: str = SchemaField(
description="The key for the new entry.", placeholder="new_key"
)
value: Any = SchemaField(
description="The value for the new entry.", placeholder="new_value"
)
class Output(BlockSchema):
updated_dictionary: dict = SchemaField(
description="The dictionary with the new entry added."
)
error: str = SchemaField(description="Error message if the operation failed.")
def __init__(self):
super().__init__(
id="31d1064e-7446-4693-a7d4-65e5ca1180d1",
description="Adds a new key-value pair to a dictionary. If no dictionary is provided, a new one is created.",
categories={BlockCategory.BASIC},
input_schema=DictionaryAddEntryBlock.Input,
output_schema=DictionaryAddEntryBlock.Output,
test_input=[
{
"dictionary": {"existing_key": "existing_value"},
"key": "new_key",
"value": "new_value",
},
{"key": "first_key", "value": "first_value"},
],
test_output=[
(
"updated_dictionary",
{"existing_key": "existing_value", "new_key": "new_value"},
),
("updated_dictionary", {"first_key": "first_value"}),
],
)
def run(self, input_data: Input) -> BlockOutput:
try:
# If no dictionary is provided, create a new one
if input_data.dictionary is None:
updated_dict = {}
else:
# Create a copy of the input dictionary to avoid modifying the original
updated_dict = input_data.dictionary.copy()
# Add the new key-value pair
updated_dict[input_data.key] = input_data.value
yield "updated_dictionary", updated_dict
except Exception as e:
yield "error", f"Failed to add entry to dictionary: {str(e)}"
class ListAddEntryBlock(Block):
class Input(BlockSchema):
list: List[Any] | None = SchemaField(
default=None,
description="The list to add the entry to. If not provided, a new list will be created.",
placeholder='[1, "string", {"key": "value"}]',
)
entry: Any = SchemaField(
description="The entry to add to the list. Can be of any type (string, int, dict, etc.).",
placeholder='{"new_key": "new_value"}',
)
position: int | None = SchemaField(
default=None,
description="The position to insert the new entry. If not provided, the entry will be appended to the end of the list.",
placeholder="0",
)
class Output(BlockSchema):
updated_list: List[Any] = SchemaField(
description="The list with the new entry added."
)
error: str = SchemaField(description="Error message if the operation failed.")
def __init__(self):
super().__init__(
id="aeb08fc1-2fc1-4141-bc8e-f758f183a822",
description="Adds a new entry to a list. The entry can be of any type. If no list is provided, a new one is created.",
categories={BlockCategory.BASIC},
input_schema=ListAddEntryBlock.Input,
output_schema=ListAddEntryBlock.Output,
test_input=[
{
"list": [1, "string", {"existing_key": "existing_value"}],
"entry": {"new_key": "new_value"},
"position": 1,
},
{"entry": "first_entry"},
{"list": ["a", "b", "c"], "entry": "d"},
],
test_output=[
(
"updated_list",
[
1,
{"new_key": "new_value"},
"string",
{"existing_key": "existing_value"},
],
),
("updated_list", ["first_entry"]),
("updated_list", ["a", "b", "c", "d"]),
],
)
def run(self, input_data: Input) -> BlockOutput:
try:
# If no list is provided, create a new one
if input_data.list is None:
updated_list = []
else:
# Create a copy of the input list to avoid modifying the original
updated_list = input_data.list.copy()
# Add the new entry
if input_data.position is None:
updated_list.append(input_data.entry)
else:
updated_list.insert(input_data.position, input_data.entry)
yield "updated_list", updated_list
except Exception as e:
yield "error", f"Failed to add entry to list: {str(e)}"

View File

@@ -3,7 +3,6 @@ import re
from typing import Type
from autogpt_server.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from autogpt_server.util.test import execute_block_test
class BlockInstallationBlock(Block):
@@ -57,6 +56,9 @@ class BlockInstallationBlock(Block):
module = __import__(module_name, fromlist=[class_name])
block_class: Type[Block] = getattr(module, class_name)
block = block_class()
from autogpt_server.util.test import execute_block_test
execute_block_test(block)
yield "success", "Block installed successfully."
except Exception as e:

View File

@@ -0,0 +1,205 @@
import asyncio
import aiohttp
import discord
from pydantic import Field
from autogpt_server.data.block import Block, BlockOutput, BlockSchema
from autogpt_server.data.model import BlockSecret, SecretField
class DiscordReaderBlock(Block):
class Input(BlockSchema):
discord_bot_token: BlockSecret = SecretField(
key="discord_bot_token", description="Discord bot token"
)
class Output(BlockSchema):
message_content: str = Field(description="The content of the message received")
channel_name: str = Field(
description="The name of the channel the message was received from"
)
username: str = Field(
description="The username of the user who sent the message"
)
def __init__(self):
super().__init__(
id="d3f4g5h6-1i2j-3k4l-5m6n-7o8p9q0r1s2t", # Unique ID for the node
input_schema=DiscordReaderBlock.Input, # Assign input schema
output_schema=DiscordReaderBlock.Output, # Assign output schema
test_input={"discord_bot_token": "test_token"},
test_output=[
(
"message_content",
"Hello!\n\nFile from user: example.txt\nContent: This is the content of the file.",
),
("channel_name", "general"),
("username", "test_user"),
],
test_mock={
"run_bot": lambda token: asyncio.Future() # Create a Future object for mocking
},
)
async def run_bot(self, token: str):
intents = discord.Intents.default()
intents.message_content = True
client = discord.Client(intents=intents)
self.output_data = None
self.channel_name = None
self.username = None
@client.event
async def on_ready():
print(f"Logged in as {client.user}")
@client.event
async def on_message(message):
if message.author == client.user:
return
self.output_data = message.content
self.channel_name = message.channel.name
self.username = message.author.name
if message.attachments:
attachment = message.attachments[0] # Process the first attachment
if attachment.filename.endswith((".txt", ".py")):
async with aiohttp.ClientSession() as session:
async with session.get(attachment.url) as response:
file_content = await response.text()
self.output_data += f"\n\nFile from user: {attachment.filename}\nContent: {file_content}"
await client.close()
await client.start(token)
def run(self, input_data: "DiscordReaderBlock.Input") -> BlockOutput:
try:
loop = asyncio.get_event_loop()
future = self.run_bot(input_data.discord_bot_token.get_secret_value())
# If it's a Future (mock), set the result
if isinstance(future, asyncio.Future):
future.set_result(
{
"output_data": "Hello!\n\nFile from user: example.txt\nContent: This is the content of the file.",
"channel_name": "general",
"username": "test_user",
}
)
result = loop.run_until_complete(future)
# For testing purposes, use the mocked result
if isinstance(result, dict):
self.output_data = result.get("output_data")
self.channel_name = result.get("channel_name")
self.username = result.get("username")
if (
self.output_data is None
or self.channel_name is None
or self.username is None
):
raise ValueError("No message, channel name, or username received.")
yield "message_content", self.output_data
yield "channel_name", self.channel_name
yield "username", self.username
except discord.errors.LoginFailure as login_err:
raise ValueError(f"Login error occurred: {login_err}")
except Exception as e:
raise ValueError(f"An error occurred: {e}")
class DiscordMessageSenderBlock(Block):
class Input(BlockSchema):
discord_bot_token: BlockSecret = SecretField(
key="discord_bot_token", description="Discord bot token"
)
message_content: str = Field(description="The content of the message received")
channel_name: str = Field(
description="The name of the channel the message was received from"
)
class Output(BlockSchema):
status: str = Field(
description="The status of the operation (e.g., 'Message sent', 'Error')"
)
def __init__(self):
super().__init__(
id="h1i2j3k4-5l6m-7n8o-9p0q-r1s2t3u4v5w6", # Unique ID for the node
input_schema=DiscordMessageSenderBlock.Input, # Assign input schema
output_schema=DiscordMessageSenderBlock.Output, # Assign output schema
test_input={
"discord_bot_token": "YOUR_DISCORD_BOT_TOKEN",
"channel_name": "general",
"message_content": "Hello, Discord!",
},
test_output=[("status", "Message sent")],
test_mock={
"send_message": lambda token, channel_name, message_content: asyncio.Future()
},
)
async def send_message(self, token: str, channel_name: str, message_content: str):
intents = discord.Intents.default()
intents.guilds = True # Required for fetching guild/channel information
client = discord.Client(intents=intents)
@client.event
async def on_ready():
print(f"Logged in as {client.user}")
for guild in client.guilds:
for channel in guild.text_channels:
if channel.name == channel_name:
# Split message into chunks if it exceeds 2000 characters
for chunk in self.chunk_message(message_content):
await channel.send(chunk)
self.output_data = "Message sent"
await client.close()
return
self.output_data = "Channel not found"
await client.close()
await client.start(token)
def chunk_message(self, message: str, limit: int = 2000) -> list:
"""Splits a message into chunks not exceeding the Discord limit."""
return [message[i : i + limit] for i in range(0, len(message), limit)]
def run(self, input_data: "DiscordMessageSenderBlock.Input") -> BlockOutput:
try:
loop = asyncio.get_event_loop()
future = self.send_message(
input_data.discord_bot_token.get_secret_value(),
input_data.channel_name,
input_data.message_content,
)
# If it's a Future (mock), set the result
if isinstance(future, asyncio.Future):
future.set_result("Message sent")
result = loop.run_until_complete(future)
# For testing purposes, use the mocked result
if isinstance(result, str):
self.output_data = result
if self.output_data is None:
raise ValueError("No status message received.")
yield "status", self.output_data
except discord.errors.LoginFailure as login_err:
raise ValueError(f"Login error occurred: {login_err}")
except Exception as e:
raise ValueError(f"An error occurred: {e}")

View File

@@ -1,6 +1,6 @@
import logging
from enum import Enum
from typing import NamedTuple
from typing import List, NamedTuple
import anthropic
import ollama
@@ -8,7 +8,7 @@ import openai
from groq import Groq
from autogpt_server.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from autogpt_server.data.model import BlockSecret, SecretField
from autogpt_server.data.model import BlockSecret, SchemaField, SecretField
from autogpt_server.util import json
logger = logging.getLogger(__name__)
@@ -409,3 +409,127 @@ class TextSummarizerBlock(Block):
).send(None)[
1
] # Get the first yielded value
class MessageRole(str, Enum):
SYSTEM = "system"
USER = "user"
ASSISTANT = "assistant"
class Message(BlockSchema):
role: MessageRole
content: str
class AdvancedLlmCallBlock(Block):
class Input(BlockSchema):
messages: List[Message] = SchemaField(
description="List of messages in the conversation.", min_items=1
)
model: LlmModel = SchemaField(
default=LlmModel.GPT4_TURBO,
description="The language model to use for the conversation.",
)
api_key: BlockSecret = SecretField(
value="", description="API key for the chosen language model provider."
)
max_tokens: int | None = SchemaField(
default=None,
description="The maximum number of tokens to generate in the chat completion.",
ge=1,
)
class Output(BlockSchema):
response: str = SchemaField(
description="The model's response to the conversation."
)
error: str = SchemaField(description="Error message if the API call failed.")
def __init__(self):
super().__init__(
id="c3d4e5f6-g7h8-i9j0-k1l2-m3n4o5p6q7r8",
description="Advanced LLM call that takes a list of messages and sends them to the language model.",
categories={BlockCategory.LLM},
input_schema=AdvancedLlmCallBlock.Input,
output_schema=AdvancedLlmCallBlock.Output,
test_input={
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Who won the world series in 2020?"},
{
"role": "assistant",
"content": "The Los Angeles Dodgers won the World Series in 2020.",
},
{"role": "user", "content": "Where was it played?"},
],
"model": LlmModel.GPT4_TURBO,
"api_key": "test_api_key",
},
test_output=(
"response",
"The 2020 World Series was played at Globe Life Field in Arlington, Texas.",
),
test_mock={
"llm_call": lambda *args, **kwargs: "The 2020 World Series was played at Globe Life Field in Arlington, Texas."
},
)
@staticmethod
def llm_call(
api_key: str,
model: LlmModel,
messages: List[dict[str, str]],
max_tokens: int | None = None,
) -> str:
provider = model.metadata.provider
if provider == "openai":
openai.api_key = api_key
response = openai.chat.completions.create(
model=model.value,
messages=messages, # type: ignore
max_tokens=max_tokens,
)
return response.choices[0].message.content or ""
elif provider == "anthropic":
client = anthropic.Anthropic(api_key=api_key)
response = client.messages.create(
model=model.value, max_tokens=max_tokens or 4096, messages=messages # type: ignore
)
return response.content[0].text if response.content else ""
elif provider == "groq":
client = Groq(api_key=api_key)
response = client.chat.completions.create(
model=model.value,
messages=messages, # type: ignore
max_tokens=max_tokens,
)
return response.choices[0].message.content or ""
elif provider == "ollama":
response = ollama.chat(
model=model.value, messages=messages, stream=False # type: ignore
)
return response["message"]["content"]
else:
raise ValueError(f"Unsupported LLM provider: {provider}")
def run(self, input_data: Input) -> BlockOutput:
try:
api_key = (
input_data.api_key.get_secret_value()
or LlmApiKeys[input_data.model.metadata.provider].get_secret_value()
)
messages = [message.model_dump() for message in input_data.messages]
response = self.llm_call(
api_key=api_key,
model=input_data.model,
messages=messages,
max_tokens=input_data.max_tokens,
)
yield "response", response
except Exception as e:
yield "error", f"Error calling LLM: {str(e)}"

View File

@@ -154,3 +154,33 @@ class TextFormatterBlock(Block):
texts=input_data.texts,
**input_data.named_texts,
)
class TextCombinerBlock(Block):
class Input(BlockSchema):
input1: str = Field(description="First text input", default="a")
input2: str = Field(description="Second text input", default="b")
class Output(BlockSchema):
output: str = Field(description="Combined text")
def __init__(self):
super().__init__(
id="e30a4d42-7b7d-4e6a-b36e-1f9b8e3b7d85",
description="This block combines multiple input texts into a single output text.",
categories={BlockCategory.TEXT},
input_schema=TextCombinerBlock.Input,
output_schema=TextCombinerBlock.Output,
test_input=[
{"input1": "Hello world I like ", "input2": "cake and to go for walks"},
{"input1": "This is a test. ", "input2": "Let's see how it works."},
],
test_output=[
("output", "Hello world I like cake and to go for walks"),
("output", "This is a test. Let's see how it works."),
],
)
def run(self, input_data: Input) -> BlockOutput:
combined_text = (input_data.input1 or "") + (input_data.input2 or "")
yield "output", combined_text

View File

@@ -22,6 +22,7 @@ class BlockCategory(Enum):
TEXT = "Block that processes text data."
SEARCH = "Block that searches or extracts information from the internet."
BASIC = "Block that performs basic operations."
INPUT_OUTPUT = "Block that interacts with input/output of the graph."
def dict(self) -> dict[str, str]:
return {"category": self.name, "description": self.value}

View File

@@ -7,7 +7,8 @@ import prisma.types
from prisma.models import AgentGraph, AgentNode, AgentNodeLink
from pydantic import PrivateAttr
from autogpt_server.data.block import BlockInput
from autogpt_server.blocks.basic import InputBlock, OutputBlock
from autogpt_server.data.block import BlockInput, get_block
from autogpt_server.data.db import BaseDbModel
from autogpt_server.util import json
@@ -92,7 +93,68 @@ class Graph(GraphMeta):
@property
def starting_nodes(self) -> list[Node]:
outbound_nodes = {link.sink_id for link in self.links}
return [node for node in self.nodes if node.id not in outbound_nodes]
input_nodes = {
v.id for v in self.nodes if isinstance(get_block(v.block_id), InputBlock)
}
return [
node
for node in self.nodes
if node.id not in outbound_nodes or node.id in input_nodes
]
@property
def ending_nodes(self) -> list[Node]:
return [v for v in self.nodes if isinstance(get_block(v.block_id), OutputBlock)]
def validate_graph(self):
def sanitize(name):
return name.split("_#_")[0].split("_@_")[0].split("_$_")[0]
# Check if all required fields are filled or connected, except for InputBlock.
for node in self.nodes:
block = get_block(node.block_id)
if block is None:
raise ValueError(f"Invalid block {node.block_id} for node #{node.id}")
provided_inputs = set(
[sanitize(name) for name in node.input_default]
+ [sanitize(link.sink_name) for link in node.input_links]
)
for name in block.input_schema.get_required_fields():
if name not in provided_inputs and not isinstance(block, InputBlock):
raise ValueError(
f"Node {block.name} #{node.id} required input missing: `{name}`"
)
# Check if all links are connected compatible pin data type.
for link in self.links:
source_id = link.source_id
sink_id = link.sink_id
suffix = f"Link {source_id}<->{sink_id}"
source_node = next((v for v in self.nodes if v.id == source_id), None)
if not source_node:
raise ValueError(f"{suffix}, {source_id} is invalid node.")
sink_node = next((v for v in self.nodes if v.id == sink_id), None)
if not sink_node:
raise ValueError(f"{suffix}, {sink_id} is invalid node.")
source_block = get_block(source_node.block_id)
if not source_block:
raise ValueError(f"{suffix}, {source_node.block_id} is invalid block.")
sink_block = get_block(sink_node.block_id)
if not sink_block:
raise ValueError(f"{suffix}, {sink_node.block_id} is invalid block.")
source_name = sanitize(link.source_name)
if source_name not in source_block.output_schema.get_fields():
raise ValueError(f"{suffix}, `{source_name}` is invalid output pin.")
sink_name = sanitize(link.sink_name)
if sink_name not in sink_block.input_schema.get_fields():
raise ValueError(f"{suffix}, `{sink_name}` is invalid input pin.")
# TODO: Add type compatibility check here.
@staticmethod
def from_db(graph: AgentGraph):

View File

@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any, Coroutine, Generator, TypeVar
if TYPE_CHECKING:
from autogpt_server.server.server import AgentServer
from autogpt_server.blocks.basic import InputBlock
from autogpt_server.data import db
from autogpt_server.data.block import Block, BlockData, BlockInput, get_block
from autogpt_server.data.execution import (
@@ -419,10 +420,16 @@ class ExecutionManager(AppService):
graph: Graph | None = self.run_and_wait(get_graph(graph_id))
if not graph:
raise Exception(f"Graph #{graph_id} not found.")
graph.validate_graph()
nodes_input = []
for node in graph.starting_nodes:
input_data, error = validate_exec(node, data)
if isinstance(get_block(node.block_id), InputBlock):
input_data = {"input": data}
else:
input_data = {}
input_data, error = validate_exec(node, input_data)
if not input_data:
raise Exception(error)
else:

View File

@@ -1,4 +1,4 @@
from autogpt_server.blocks.basic import PrintingBlock, ValueBlock
from autogpt_server.blocks.basic import InputBlock, PrintingBlock
from autogpt_server.blocks.text import TextFormatterBlock
from autogpt_server.data import graph
from autogpt_server.data.graph import create_graph
@@ -14,12 +14,18 @@ def create_test_graph() -> graph.Graph:
ValueBlock
"""
nodes = [
graph.Node(block_id=ValueBlock().id),
graph.Node(block_id=ValueBlock().id),
graph.Node(
block_id=InputBlock().id,
input_default={"key": "input_1"},
),
graph.Node(
block_id=InputBlock().id,
input_default={"key": "input_2"},
),
graph.Node(
block_id=TextFormatterBlock().id,
input_default={
"format": "{texts[0]},{texts[1]},{texts[2]}",
"format": "{texts[0]}, {texts[1]}{texts[2]}",
"texts_$_3": "!!!",
},
),
@@ -58,7 +64,7 @@ async def sample_agent():
async with SpinTestServer() as server:
exec_man = server.exec_manager
test_graph = await create_graph(create_test_graph())
input_data = {"input": "test!!"}
input_data = {"input_1": "Hello", "input_2": "World"}
response = await server.agent_server.execute_graph(test_graph.id, input_data)
print(response)
result = await wait_execution(exec_man, test_graph.id, response["id"], 4, 10)

View File

@@ -105,6 +105,8 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
medium_api_key: str = Field(default="", description="Medium API key")
medium_author_id: str = Field(default="", description="Medium author ID")
discord_bot_token: str = Field(default="", description="Discord bot token")
# Add more secret fields as needed
model_config = SettingsConfigDict(

View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
[[package]]
name = "agpt"
@@ -25,7 +25,7 @@ requests = "*"
sentry-sdk = "^1.40.4"
[package.extras]
benchmark = ["agbenchmark @ file:///Users/aarushi/autogpt/AutoGPT/benchmark"]
benchmark = ["agbenchmark @ file:///home/bently/Desktop/autogpt-ui/AutoGPT/benchmark"]
[package.source]
type = "directory"
@@ -329,7 +329,7 @@ watchdog = "4.0.0"
webdriver-manager = "^4.0.1"
[package.extras]
benchmark = ["agbenchmark @ file:///Users/aarushi/autogpt/AutoGPT/benchmark"]
benchmark = ["agbenchmark @ file:///home/bently/Desktop/autogpt-ui/AutoGPT/benchmark"]
[package.source]
type = "directory"
@@ -1073,6 +1073,26 @@ wrapt = ">=1.10,<2"
[package.extras]
dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"]
[[package]]
name = "discord-py"
version = "2.4.0"
description = "A Python wrapper for the Discord API"
optional = false
python-versions = ">=3.8"
files = [
{file = "discord.py-2.4.0-py3-none-any.whl", hash = "sha256:b8af6711c70f7e62160bfbecb55be699b5cb69d007426759ab8ab06b1bd77d1d"},
{file = "discord_py-2.4.0.tar.gz", hash = "sha256:d07cb2a223a185873a1d0ee78b9faa9597e45b3f6186df21a95cec1e9bcdc9a5"},
]
[package.dependencies]
aiohttp = ">=3.7.4,<4"
[package.extras]
docs = ["sphinx (==4.4.0)", "sphinx-inline-tabs (==2023.4.21)", "sphinxcontrib-applehelp (==1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (==2.0.1)", "sphinxcontrib-jsmath (==1.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport (==1.2.4)", "typing-extensions (>=4.3,<5)"]
speed = ["Brotli", "aiodns (>=1.1)", "cchardet (==2.1.7)", "orjson (>=3.5.4)"]
test = ["coverage[toml]", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock", "typing-extensions (>=4.3,<5)", "tzdata"]
voice = ["PyNaCl (>=1.3.0,<1.6)"]
[[package]]
name = "distro"
version = "1.9.0"
@@ -6399,4 +6419,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools",
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "b9a68db94e5721bfc916e583626e6da66a3469451d179bf9ee117d0a31b865f2"
content-hash = "9991857e7076d3bfcbae7af6c2cec54dc943167a3adceb5a0ebf74d80c05778f"

View File

@@ -39,6 +39,7 @@ ollama = "^0.3.0"
feedparser = "^6.0.11"
python-dotenv = "^1.0.1"
expiringdict = "^1.2.2"
discord-py = "^2.4.0"
autogpt-libs = { path = "../autogpt_libs", develop = true }
[tool.poetry.group.dev.dependencies]

View File

@@ -28,37 +28,37 @@ async def execute_graph(
async def assert_sample_graph_executions(
agent_server: AgentServer, test_graph: graph.Graph, graph_exec_id: str
):
text = "Hello, World!"
input = {"input_1": "Hello", "input_2": "World"}
executions = await agent_server.get_run_execution_results(
test_graph.id, graph_exec_id
)
# Executing ConstantBlock1
# Executing ValueBlock
exec = executions[0]
assert exec.status == execution.ExecutionStatus.COMPLETED
assert exec.graph_exec_id == graph_exec_id
assert exec.output_data == {"output": ["Hello, World!"]}
assert exec.input_data == {"input": text}
assert exec.output_data == {"output": ["Hello"]}
assert exec.input_data == {"input": input, "key": "input_1"}
assert exec.node_id in [test_graph.nodes[0].id, test_graph.nodes[1].id]
# Executing ConstantBlock2
# Executing ValueBlock
exec = executions[1]
assert exec.status == execution.ExecutionStatus.COMPLETED
assert exec.graph_exec_id == graph_exec_id
assert exec.output_data == {"output": ["Hello, World!"]}
assert exec.input_data == {"input": text}
assert exec.output_data == {"output": ["World"]}
assert exec.input_data == {"input": input, "key": "input_2"}
assert exec.node_id in [test_graph.nodes[0].id, test_graph.nodes[1].id]
# Executing TextFormatterBlock
exec = executions[2]
assert exec.status == execution.ExecutionStatus.COMPLETED
assert exec.graph_exec_id == graph_exec_id
assert exec.output_data == {"output": ["Hello, World!,Hello, World!,!!!"]}
assert exec.output_data == {"output": ["Hello, World!!!"]}
assert exec.input_data == {
"format": "{texts[0]},{texts[1]},{texts[2]}",
"texts": ["Hello, World!", "Hello, World!", "!!!"],
"texts_$_1": "Hello, World!",
"texts_$_2": "Hello, World!",
"format": "{texts[0]}, {texts[1]}{texts[2]}",
"texts": ["Hello", "World", "!!!"],
"texts_$_1": "Hello",
"texts_$_2": "World",
"texts_$_3": "!!!",
}
assert exec.node_id == test_graph.nodes[2].id
@@ -68,7 +68,7 @@ async def assert_sample_graph_executions(
assert exec.status == execution.ExecutionStatus.COMPLETED
assert exec.graph_exec_id == graph_exec_id
assert exec.output_data == {"status": ["printed"]}
assert exec.input_data == {"text": "Hello, World!,Hello, World!,!!!"}
assert exec.input_data == {"text": "Hello, World!!!"}
assert exec.node_id == test_graph.nodes[3].id
@@ -76,7 +76,7 @@ async def assert_sample_graph_executions(
async def test_agent_execution(server):
test_graph = create_test_graph()
await graph.create_graph(test_graph)
data = {"input": "Hello, World!"}
data = {"input_1": "Hello", "input_2": "World"}
graph_exec_id = await execute_graph(
server.agent_server, server.exec_manager, test_graph, data, 4
)

View File

@@ -10,21 +10,35 @@ else
if ! command -v python3 &> /dev/null
then
echo "python3 could not be found"
echo "Installing python3 using pyenv..."
if ! command -v pyenv &> /dev/null
then
echo "pyenv could not be found"
echo "Installing pyenv..."
curl https://pyenv.run | bash
echo "Install python3 using pyenv ([y]/n)?"
read response
if [[ "$response" == "y" || -z "$response" ]]; then
echo "Installing python3..."
if ! command -v pyenv &> /dev/null
then
echo "pyenv could not be found"
echo "Installing pyenv..."
curl https://pyenv.run | bash
fi
pyenv install 3.11.5
pyenv global 3.11.5
else
echo "Aborting setup"
exit 1
fi
pyenv install 3.11.5
pyenv global 3.11.5
fi
if ! command -v poetry &> /dev/null
then
echo "poetry could not be found"
echo "Installing poetry..."
curl -sSL https://install.python-poetry.org | python3 -
echo "Install poetry using official installer ([y]/n)?"
read response
if [[ "$response" == "y" || -z "$response" ]]; then
echo "Installing poetry..."
curl -sSL https://install.python-poetry.org | python3 -
else
echo "Aborting setup"
exit 1
fi
fi
fi