Merge branch 'aarushikansal/open-1646-ci-for-docker' into aarushikansal/open-1576-helm-install-in-ci

This commit is contained in:
Aarushi 2024-09-12 09:36:33 +01:00 committed by GitHub
commit 8171d12164
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
265 changed files with 20228 additions and 4590 deletions

View File

@ -34,5 +34,7 @@ rnd/autogpt_builder/.next/
rnd/autogpt_builder/node_modules
rnd/autogpt_builder/.env.example
rnd/autogpt_builder/.env.local
rnd/autogpt_server/.env
rnd/autogpt_server/.venv/
rnd/market/.env

View File

@ -31,12 +31,10 @@ jobs:
matrix:
python-version: ["3.10"]
platform-os: [ubuntu, macos, macos-arm64, windows]
db-platform: [postgres, sqlite]
runs-on: ${{ matrix.platform-os != 'macos-arm64' && format('{0}-latest', matrix.platform-os) || 'macos-14' }}
steps:
- name: Setup PostgreSQL
if: matrix.db-platform == 'postgres'
uses: ikalnytskyi/action-setup-postgres@v6
with:
username: ${{ secrets.DB_USER || 'postgres' }}
@ -116,31 +114,28 @@ jobs:
- name: Install Python dependencies
run: poetry install
- name: Generate Prisma Client (Postgres)
if: matrix.db-platform == 'postgres'
run: poetry run prisma generate --schema postgres/schema.prisma
- name: Generate Prisma Client
run: poetry run prisma generate
- name: Run Database Migrations (Postgres)
if: matrix.db-platform == 'postgres'
run: poetry run prisma migrate dev --schema postgres/schema.prisma --name updates
- name: Run Database Migrations
run: poetry run prisma migrate dev --name updates
env:
CONNECTION_STR: ${{ steps.postgres.outputs.connection-uri }}
- name: Generate Prisma Client (SQLite)
if: matrix.db-platform == 'sqlite'
run: poetry run prisma generate
- name: Run Database Migrations (SQLite)
if: matrix.db-platform == 'sqlite'
run: poetry run prisma migrate dev --name updates
- name: Run Linter
- id: lint
name: Run Linter
run: poetry run lint
- name: Run pytest with coverage
run: |
poetry run pytest -vv \
test
if [[ "${{ runner.debug }}" == "1" ]]; then
poetry run pytest -vv -o log_cli=true -o log_cli_level=DEBUG test
else
poetry run pytest -vv test
fi
if: success() || (failure() && steps.lint.outcome == 'failure')
env:
LOG_LEVEL: ${{ runner.debug && 'DEBUG' || 'INFO' }}
env:
CI: true
PLAIN_OUTPUT: True

View File

@ -1,55 +0,0 @@
import os
import requests
import sys
# GitHub API endpoint
api_url = os.environ["GITHUB_API_URL"]
repo = os.environ["GITHUB_REPOSITORY"]
sha = os.environ["GITHUB_SHA"]
# GitHub token for authentication
github_token = os.environ["GITHUB_TOKEN"]
# API endpoint for check runs for the specific SHA
endpoint = f"{api_url}/repos/{repo}/commits/{sha}/check-runs"
# Set up headers for authentication
headers = {
"Authorization": f"token {github_token}",
"Accept": "application/vnd.github.v3+json"
}
# Make the API request
response = requests.get(endpoint, headers=headers)
if response.status_code != 200:
print(f"Error: Unable to fetch check runs data. Status code: {response.status_code}")
sys.exit(1)
check_runs = response.json()["check_runs"]
# Flag to track if all other check runs have passed
all_others_passed = True
# Current run id
current_run_id = os.environ["GITHUB_RUN_ID"]
for run in check_runs:
if str(run["id"]) != current_run_id:
status = run["status"]
conclusion = run["conclusion"]
if status == "completed":
if conclusion not in ["success", "skipped", "neutral"]:
all_others_passed = False
print(f"Check run {run['name']} (ID: {run['id']}) has conclusion: {conclusion}")
else:
print(f"Check run {run['name']} (ID: {run['id']}) is still {status}.")
all_others_passed = False
if all_others_passed:
print("All other completed check runs have passed. This check passes.")
sys.exit(0)
else:
print("Some check runs have failed or have not completed. This check fails.")
sys.exit(1)

View File

@ -1,51 +1,31 @@
name: PR Status Checker
on:
workflow_run:
workflows: ["*"]
types:
- completed
pull_request:
types: [opened, synchronize, reopened]
jobs:
status-check:
name: Check Actions Status
name: Check PR Status
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Debug Information
run: |
echo "Event name: ${{ github.event_name }}"
echo "Workflow: ${{ github.workflow }}"
echo "Action: ${{ github.action }}"
echo "Actor: ${{ github.actor }}"
echo "Repository: ${{ github.repository }}"
echo "Ref: ${{ github.ref }}"
echo "Head ref: ${{ github.head_ref }}"
echo "Base ref: ${{ github.base_ref }}"
echo "Event payload:"
cat $GITHUB_EVENT_PATH
- name: Debug File Structure
run: |
echo "Current directory:"
pwd
echo "Directory contents:"
ls -R
echo "GitHub workspace:"
echo $GITHUB_WORKSPACE
echo "GitHub workspace contents:"
ls -R $GITHUB_WORKSPACE
- name: Check Actions Status
run: |
echo "Current directory before running Python script:"
pwd
echo "Attempting to run Python script:"
python .github/scripts/check_actions_status.py
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# - name: Wait some time for all actions to start
# run: sleep 30
- uses: actions/checkout@v4
# with:
# fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Check PR Status
run: |
echo "Current directory before running Python script:"
pwd
echo "Attempting to run Python script:"
python check_actions_status.py
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -32,6 +32,14 @@
"name": "market",
"path": "../rnd/market"
},
{
"name": "lib",
"path": "../rnd/autogpt_libs"
},
{
"name": "infra",
"path": "../rnd/infra"
},
{
"name": "[root]",
"path": ".."

109
check_actions_status.py Normal file
View File

@ -0,0 +1,109 @@
import json
import os
import requests
import sys
import time
from typing import Dict, List, Tuple
def get_environment_variables() -> Tuple[str, str, str, str, str]:
"""Retrieve and return necessary environment variables."""
try:
with open(os.environ["GITHUB_EVENT_PATH"]) as f:
event = json.load(f)
sha = event["pull_request"]["head"]["sha"]
return (
os.environ["GITHUB_API_URL"],
os.environ["GITHUB_REPOSITORY"],
sha,
os.environ["GITHUB_TOKEN"],
os.environ["GITHUB_RUN_ID"],
)
except KeyError as e:
print(f"Error: Missing required environment variable or event data: {e}")
sys.exit(1)
def make_api_request(url: str, headers: Dict[str, str]) -> Dict:
"""Make an API request and return the JSON response."""
try:
print("Making API request to:", url)
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
print(f"Error: API request failed. {e}")
sys.exit(1)
def process_check_runs(check_runs: List[Dict]) -> Tuple[bool, bool]:
"""Process check runs and return their status."""
runs_in_progress = False
all_others_passed = True
for run in check_runs:
if str(run["name"]) != "Check PR Status":
status = run["status"]
conclusion = run["conclusion"]
if status == "completed":
if conclusion not in ["success", "skipped", "neutral"]:
all_others_passed = False
print(
f"Check run {run['name']} (ID: {run['id']}) has conclusion: {conclusion}"
)
else:
runs_in_progress = True
print(f"Check run {run['name']} (ID: {run['id']}) is still {status}.")
all_others_passed = False
else:
print(
f"Skipping check run {run['name']} (ID: {run['id']}) as it is the current run."
)
return runs_in_progress, all_others_passed
def main():
api_url, repo, sha, github_token, current_run_id = get_environment_variables()
endpoint = f"{api_url}/repos/{repo}/commits/{sha}/check-runs"
headers = {
"Accept": "application/vnd.github.v3+json",
}
if github_token:
headers["Authorization"] = f"token {github_token}"
print(f"Current run ID: {current_run_id}")
while True:
data = make_api_request(endpoint, headers)
check_runs = data["check_runs"]
print("Processing check runs...")
print(check_runs)
runs_in_progress, all_others_passed = process_check_runs(check_runs)
if not runs_in_progress:
break
print(
"Some check runs are still in progress. Waiting 3 minutes before checking again..."
)
time.sleep(180)
if all_others_passed:
print("All other completed check runs have passed. This check passes.")
sys.exit(0)
else:
print("Some check runs have failed or have not completed. This check fails.")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -69,6 +69,8 @@ Lets the agent execute non-interactive Shell commands and Python code. Python ex
| `shell_denylist` | List of prohibited shell commands | `List[str]` | `[]` |
| `docker_container_name` | Name of the Docker container used for code execution | `str` | `"agent_sandbox"` |
All shell command configurations are expected to be for convience only. This component is not secure and should not be used in production environments. It is recommended to use more appropriate sandboxing.
### CommandProvider
- `execute_shell` execute shell command

View File

@ -0,0 +1,17 @@
# Find available voices for D-ID
1. **ElevenLabs**
- Select any voice from the voice list: https://api.elevenlabs.io/v1/voices
- Copy the voice_id
- Use it as a string in the voice_id field in the CreateTalkingAvatarClip Block
2. **Microsoft Azure Voices**
- Select any voice from the voice gallery: https://speech.microsoft.com/portal/voicegallery
- Click on the "Sample code" tab on the right
- Copy the voice name, for example: config.SpeechSynthesisVoiceName ="en-GB-AbbiNeural"
- Use this string en-GB-AbbiNeural in the voice_id field in the CreateTalkingAvatarClip Block
3. **Amazon Polly Voices**
- Select any voice from the voice list: https://docs.aws.amazon.com/polly/latest/dg/available-voices.html
- Copy the voice name / ID
- Use it as string in the voice_id field in the CreateTalkingAvatarClip Block

View File

@ -56,6 +56,16 @@ Poetry is a package manager for Python. You can install it by running the follow
```bash
pip install poetry
```
- Installing Docker and Docker Compose
Docker containerizes applications, while Docker Compose orchestrates multi-container Docker applications.
You can follow the steps here:
If you need assistance installing docker:
https://docs.docker.com/desktop/
If you need assistance installing docker compose:
https://docs.docker.com/compose/install/
### Installing the dependencies
@ -63,6 +73,7 @@ Once you have installed Yarn and Poetry, you can run the following command to in
```bash
cd rnd/autogpt_server
cp .env.example .env
poetry install
```
@ -77,21 +88,38 @@ Once you have installed the dependencies, you can proceed to the next step.
### Setting up the database
In order to setup the database, you need to run the following command, in the same terminal you ran the `poetry install` command:
In order to setup the database, you need to run the following commands, in the same terminal you ran the `poetry install` command:
```sh
docker compose up postgres redis -d
poetry run prisma migrate dev
```
After deploying the migration, to ensure that the database schema is correctly mapped to your codebase, allowing the application to interact with the database properly, you need to generate the Prisma database model:
```bash
poetry run prisma migrate deploy
poetry run prisma generate
```
### Running the server
Without running this command, the necessary Python modules (prisma.models) won't be available, leading to a `ModuleNotFoundError`.
To run the server, you can run the following command in the same terminal you ran the `poetry install` command:
### Running the server without Docker
To run the server, you can run the following commands in the same terminal you ran the `poetry install` command:
```bash
poetry run app
```
In the other terminal, you can run the following command to start the frontend:
### Running the server within Docker
To run the server, you can run the following commands in the same terminal you ran the `poetry install` command:
```bash
docker compose build
docker compose up
```
In the other terminal from autogpt_builder, you can run the following command to start the frontend:
```bash
yarn dev
@ -100,3 +128,10 @@ yarn dev
### Checking if the server is running
You can check if the server is running by visiting [http://localhost:3000](http://localhost:3000) in your browser.
### Notes:
By default the daemons for different services run on the following ports:
Execution Manager Daemon: 8002
Execution Scheduler Daemon: 8003
Rest Server Daemon: 8004

View File

@ -10,6 +10,7 @@ nav:
- Setup: server/setup.md
- Advanced Setup: server/advanced_setup.md
- Using Ollama: server/ollama.md
- Using D-ID: serveer/d_id.md
- AutoGPT Agent:
- Introduction: AutoGPT/index.md

View File

@ -1,36 +1,114 @@
This is a guide to setting up and running the AutoGPT Server and Builder. This tutorial will cover downloading the necessary files, setting up the server, and testing the system.
# AutoGPT Platform
https://github.com/user-attachments/assets/fd0d0f35-3155-4263-b575-ba3efb126cb4
Welcome to the AutoGPT Platform - a powerful system for creating and running AI agents to solve business problems. This platform enables you to harness the power of artificial intelligence to automate tasks, analyze data, and generate insights for your organization.
1. Navigate to the AutoGPT GitHub repository.
2. Click the "Code" button, then select "Download ZIP".
3. Once downloaded, extract the ZIP file to a folder of your choice.
## Getting Started
4. Open the extracted folder and navigate to the "rnd" directory.
5. Enter the "AutoGPT server" folder.
6. Open a terminal window in this directory.
7. Locate and open the README file in the AutoGPT server folder: [doc](./autogpt_server/README.md#setup).
8. Copy and paste each command from the setup section in the README into your terminal.
- Important: Wait for each command to finish before running the next one.
9. If all commands run without errors, enter the final command: `poetry run app`
10. You should now see the server running in your terminal.
### Prerequisites
- Docker
- Docker Compose V2 (comes with Docker Desktop, or can be installed separately)
### Running the System
To run the AutoGPT Platform, follow these steps:
1. Clone this repository to your local machine.
2. Navigate to the project directory.
3. Run the following command:
```
docker compose up -d
```
This command will start all the necessary services defined in the `docker-compose.yml` file in detached mode.
### Docker Compose Commands
Here are some useful Docker Compose commands for managing your AutoGPT Platform:
- `docker compose up -d`: Start the services in detached mode.
- `docker compose stop`: Stop the running services without removing them.
- `docker compose rm`: Remove stopped service containers.
- `docker compose build`: Build or rebuild services.
- `docker compose down`: Stop and remove containers, networks, and volumes.
- `docker compose watch`: Watch for changes in your services and automatically update them.
### Sample Scenarios
Here are some common scenarios where you might use multiple Docker Compose commands:
1. Updating and restarting a specific service:
```
docker compose build api_srv
docker compose up -d --no-deps api_srv
```
This rebuilds the `api_srv` service and restarts it without affecting other services.
2. Viewing logs for troubleshooting:
```
docker compose logs -f api_srv ws_srv
```
This shows and follows the logs for both `api_srv` and `ws_srv` services.
3. Scaling a service for increased load:
```
docker compose up -d --scale executor=3
```
This scales the `executor` service to 3 instances to handle increased load.
4. Stopping the entire system for maintenance:
```
docker compose stop
docker compose rm -f
docker compose pull
docker compose up -d
```
This stops all services, removes containers, pulls the latest images, and restarts the system.
5. Developing with live updates:
```
docker compose watch
```
This watches for changes in your code and automatically updates the relevant services.
6. Checking the status of services:
```
docker compose ps
```
This shows the current status of all services defined in your docker-compose.yml file.
These scenarios demonstrate how to use Docker Compose commands in combination to manage your AutoGPT Platform effectively.
### Persisting Data
To persist data for PostgreSQL and Redis, you can modify the `docker-compose.yml` file to add volumes. Here's how:
1. Open the `docker-compose.yml` file in a text editor.
2. Add volume configurations for PostgreSQL and Redis services:
```yaml
services:
postgres:
# ... other configurations ...
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
# ... other configurations ...
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
```
3. Save the file and run `docker compose up -d` to apply the changes.
This configuration will create named volumes for PostgreSQL and Redis, ensuring that your data persists across container restarts.
11. Navigate back to the "rnd" folder.
12. Open the "AutoGPT builder" folder.
13. Open the README file in this folder: [doc](./autogpt_builder/README.md#getting-started).
14. In your terminal, run the following commands:
```
npm install
```
```
npm run dev
```
15. Once the front-end is running, click the link to navigate to `localhost:3000`.
16. Click on the "Build" option.
17. Add a few blocks to test the functionality.
18. Connect the blocks together.
19. Click "Run".
20. Check your terminal window - you should see that the server has received the request, is processing it, and has executed it.
And there you have it! You've successfully set up and tested AutoGPT.

View File

@ -1,5 +1,6 @@
NEXT_PUBLIC_AGPT_SERVER_URL=http://localhost:8000/api
NEXT_PUBLIC_AGPT_MARKETPLACE_URL=http://localhost:8001/api/v1/market
NEXT_PUBLIC_AGPT_WS_SERVER_URL=ws://localhost:8001/ws
NEXT_PUBLIC_AGPT_MARKETPLACE_URL=http://localhost:8005/api/v1/market
## Supabase credentials
## YOU ONLY NEED THEM IF YOU WANT TO USE SUPABASE USER AUTHENTICATION
@ -11,3 +12,4 @@ NEXT_PUBLIC_AGPT_MARKETPLACE_URL=http://localhost:8001/api/v1/market
## This should be {domain}/auth/callback
## Only used if you're using Supabase and OAuth
AUTH_CALLBACK_URL=http://localhost:3000/auth/callback
GA_MEASUREMENT_ID=G-FH2XK2W4GN

View File

@ -0,0 +1,3 @@
{
"plugins": ["prettier-plugin-tailwindcss"]
}

View File

@ -1,19 +1,19 @@
# Base stage for both dev and prod
FROM node:21-alpine AS base
WORKDIR /app
COPY autogpt_builder/package.json autogpt_builder/yarn.lock ./
COPY rnd/autogpt_builder/package.json rnd/autogpt_builder/yarn.lock ./
RUN yarn install --frozen-lockfile
# Dev stage
FROM base AS dev
ENV NODE_ENV=development
COPY autogpt_builder/ .
COPY rnd/autogpt_builder/ .
EXPOSE 3000
CMD ["npm", "run", "dev"]
CMD ["yarn", "run", "dev"]
# Build stage for prod
FROM base AS build
COPY autogpt_builder/ .
COPY rnd/autogpt_builder/ .
RUN npm run build
# Prod stage
@ -26,7 +26,7 @@ RUN yarn install --frozen-lockfile
COPY --from=build /app/.next ./.next
COPY --from=build /app/public ./public
COPY --from=build /app/next.config.js ./next.config.js
COPY --from=build /app/next.config.mjs ./next.config.mjs
EXPOSE 3000
CMD ["npm", "start"]

View File

@ -10,15 +10,22 @@ const nextConfig = {
NEXT_PUBLIC_AGPT_MARKETPLACE_URL:
process.env.NEXT_PUBLIC_AGPT_MARKETPLACE_URL,
},
images: {
domains: ["images.unsplash.com"],
},
async redirects() {
return [
{
source: "/",
destination: "/build",
source: "/monitor", // FIXME: Remove after 2024-09-01
destination: "/",
permanent: false,
},
];
},
// TODO: Re-enable TypeScript checks once current issues are resolved
typescript: {
ignoreBuildErrors: true,
},
};
export default nextConfig;

View File

@ -11,24 +11,30 @@
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@next/third-parties": "^14.2.5",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@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-select": "^2.1.1",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.2",
"@supabase/ssr": "^0.4.0",
"@supabase/supabase-js": "^2.45.0",
"@tanstack/react-table": "^8.20.5",
"@xyflow/react": "^12.1.0",
"ajv": "^8.17.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"date-fns": "^3.6.0",
"dotenv": "^16.4.5",
"lucide-react": "^0.407.0",
@ -42,10 +48,11 @@
"react-icons": "^5.2.1",
"react-markdown": "^9.0.1",
"react-modal": "^3.16.1",
"reactflow": "^11.11.4",
"react-shepherd": "^6.1.1",
"recharts": "^2.12.7",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^10.0.0",
"zod": "^3.23.8"
},
"devDependencies": {
@ -57,6 +64,7 @@
"eslint-config-next": "14.2.4",
"postcss": "^8",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.6",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}

View File

@ -0,0 +1,18 @@
import { withRoleAccess } from "@/lib/withRoleAccess";
import React from "react";
function AdminDashboard() {
return (
<div>
<h1>Admin Dashboard</h1>
{/* Add your admin-only content here */}
</div>
);
}
export default async function AdminDashboardPage() {
"use server";
const withAdminAccess = await withRoleAccess(["admin"]);
const ProtectedAdminDashboard = await withAdminAccess(AdminDashboard);
return <ProtectedAdminDashboard />;
}

View File

@ -0,0 +1,100 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { BinaryIcon, XIcon } from "lucide-react";
import { usePathname } from "next/navigation"; // Add this import
const tabs = [
{ name: "Dashboard", href: "/admin/dashboard" },
{ name: "Marketplace", href: "/admin/marketplace" },
{ name: "Users", href: "/admin/users" },
{ name: "Settings", href: "/admin/settings" },
];
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
const pathname = usePathname(); // Get the current pathname
const [activeTab, setActiveTab] = useState(() => {
// Set active tab based on the current route
return tabs.find((tab) => tab.href === pathname)?.name || tabs[0].name;
});
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
return (
<div className="min-h-screen bg-gray-100">
<nav className="bg-white shadow-sm">
<div className="max-w-10xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex h-16 items-center justify-between">
<div className="flex items-center">
<div className="flex-shrink-0">
<h1 className="text-xl font-bold">Admin Panel</h1>
</div>
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
{tabs.map((tab) => (
<Link
key={tab.name}
href={tab.href}
className={`${
activeTab === tab.name
? "border-indigo-500 text-indigo-600"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700"
} inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium`}
onClick={() => setActiveTab(tab.name)}
>
{tab.name}
</Link>
))}
</div>
</div>
<div className="sm:hidden">
<button
type="button"
className="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
<span className="sr-only">Open main menu</span>
{mobileMenuOpen ? (
<XIcon className="block h-6 w-6" aria-hidden="true" />
) : (
<BinaryIcon className="block h-6 w-6" aria-hidden="true" />
)}
</button>
</div>
</div>
</div>
{mobileMenuOpen && (
<div className="sm:hidden">
<div className="space-y-1 pb-3 pt-2">
{tabs.map((tab) => (
<Link
key={tab.name}
href={tab.href}
className={`${
activeTab === tab.name
? "border-indigo-500 bg-indigo-50 text-indigo-700"
: "border-transparent text-gray-600 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-800"
} block border-l-4 py-2 pl-3 pr-4 text-base font-medium`}
onClick={() => {
setActiveTab(tab.name);
setMobileMenuOpen(false);
}}
>
{tab.name}
</Link>
))}
</div>
</div>
)}
</nav>
<main className="py-10">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">{children}</div>
</main>
</div>
);
}

View File

@ -0,0 +1,25 @@
import { withRoleAccess } from "@/lib/withRoleAccess";
import React from "react";
import { getReviewableAgents } from "@/components/admin/marketplace/actions";
import AdminMarketplaceAgentList from "@/components/admin/marketplace/AdminMarketplaceAgentList";
import AdminFeaturedAgentsControl from "@/components/admin/marketplace/AdminFeaturedAgentsControl";
import { Separator } from "@/components/ui/separator";
async function AdminMarketplace() {
const reviewableAgents = await getReviewableAgents();
return (
<>
<AdminMarketplaceAgentList agents={reviewableAgents.agents} />
<Separator className="my-4" />
<AdminFeaturedAgentsControl className="mt-4" />
</>
);
}
export default async function AdminDashboardPage() {
"use server";
const withAdminAccess = await withRoleAccess(["admin"]);
const ProtectedAdminMarketplace = await withAdminAccess(AdminMarketplace);
return <ProtectedAdminMarketplace />;
}

View File

@ -0,0 +1,18 @@
import { withRoleAccess } from "@/lib/withRoleAccess";
import React from "react";
function AdminSettings() {
return (
<div>
<h1>Admin Settings</h1>
{/* Add your admin-only settings content here */}
</div>
);
}
export default async function AdminSettingsPage() {
"use server";
const withAdminAccess = await withRoleAccess(["admin"]);
const ProtectedAdminSettings = await withAdminAccess(AdminSettings);
return <ProtectedAdminSettings />;
}

View File

@ -0,0 +1,18 @@
import { withRoleAccess } from "@/lib/withRoleAccess";
import React from "react";
function AdminUsers() {
return (
<div>
<h1>Users Dashboard</h1>
{/* Add your admin-only content here */}
</div>
);
}
export default async function AdminUsersPage() {
"use server";
const withAdminAccess = await withRoleAccess(["admin"]);
const ProtectedAdminUsers = await withAdminAccess(AdminUsers);
return <ProtectedAdminUsers />;
}

View File

@ -8,7 +8,7 @@ export default function Home() {
return (
<FlowEditor
className="flow-container w-full min-h-[86vh] border border-gray-300 dark:border-gray-700 rounded-lg bg-secondary"
className="flow-container w-full min-h-[86vh] border border-gray-300 dark:border-gray-700 rounded-lg"
flowID={query.get("flowID") ?? query.get("templateID") ?? undefined}
template={!!query.get("templateID")}
/>

View File

@ -0,0 +1,43 @@
"use client";
import { useEffect } from "react";
import { IconCircleAlert } from "@/components/ui/icons";
import { Button } from "@/components/ui/button";
import Link from "next/link";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div className="fixed inset-0 flex items-center justify-center bg-background">
<div className="w-full max-w-md px-4 text-center sm:px-6">
<div className="mx-auto flex size-12 items-center justify-center rounded-full bg-muted">
<IconCircleAlert className="size-10" />
</div>
<h1 className="mt-8 text-2xl font-bold tracking-tight text-foreground">
Oops, something went wrong!
</h1>
<p className="mt-4 text-muted-foreground">
We&apos;re sorry, but an unexpected error has occurred. Please try
again later or contact support if the issue persists.
</p>
<div className="mt-6 flex flex-row justify-center gap-4">
<Button onClick={reset} variant="outline">
Retry
</Button>
<Button>
<Link href="/">Go to Homepage</Link>
</Button>
</div>
</div>
</div>
);
}

View File

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

View File

@ -7,6 +7,8 @@ import { cn } from "@/lib/utils";
import "./globals.css";
import TallyPopupSimple from "@/components/TallyPopup";
import { GoogleAnalytics } from "@next/third-parties/google";
import { Toaster } from "@/components/ui/toaster";
const inter = Inter({ subsets: ["latin"] });
@ -30,13 +32,18 @@ export default function RootLayout({
// enableSystem
disableTransitionOnChange
>
<div className="flex flex-col min-h-screen ">
<div className="flex min-h-screen flex-col">
<NavBar />
<main className="flex-1 p-4 overflow-hidden">{children}</main>
<main className="flex-1 overflow-hidden p-4">{children}</main>
<TallyPopupSimple />
</div>
<Toaster />
</Providers>
</body>
<GoogleAnalytics
gaId={process.env.GA_MEASUREMENT_ID || "G-FH2XK2W4GN"} // This is the measurement Id for the Google Analytics dev project
/>
</html>
);
}

View File

@ -0,0 +1,21 @@
import AgentFlowListSkeleton from "@/components/monitor/skeletons/AgentFlowListSkeleton";
import React from "react";
import FlowRunsListSkeleton from "@/components/monitor/skeletons/FlowRunsListSkeleton";
import FlowRunsStatusSkeleton from "@/components/monitor/skeletons/FlowRunsStatusSkeleton";
export default function MonitorLoadingSkeleton() {
return (
<div className="space-y-4 p-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
{/* Agents Section */}
<AgentFlowListSkeleton />
{/* Runs Section */}
<FlowRunsListSkeleton />
{/* Stats Section */}
<FlowRunsStatusSkeleton />
</div>
</div>
);
}

View File

@ -20,10 +20,15 @@ import { FaGoogle, FaGithub, FaDiscord, FaSpinner } from "react-icons/fa";
import { useState } from "react";
import { useSupabase } from "@/components/SupabaseProvider";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { Checkbox } from "@/components/ui/checkbox";
const loginFormSchema = z.object({
email: z.string().email().min(2).max(64),
password: z.string().min(6).max(64),
agreeToTerms: z.boolean().refine((value) => value === true, {
message: "You must agree to the Terms of Service and Privacy Policy",
}),
});
export default function LoginPage() {
@ -38,6 +43,7 @@ export default function LoginPage() {
defaultValues: {
email: "",
password: "",
agreeToTerms: false,
},
});
@ -48,7 +54,7 @@ export default function LoginPage() {
if (isUserLoading || isSupabaseLoading || user) {
return (
<div className="flex justify-center items-center h-[80vh]">
<div className="flex h-[80vh] items-center justify-center">
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
</div>
);
@ -71,11 +77,6 @@ export default function LoginPage() {
redirectTo:
process.env.AUTH_CALLBACK_URL ??
`http://localhost:3000/auth/callback`,
// Get Google provider_refresh_token
// queryParams: {
// access_type: 'offline',
// prompt: 'consent',
// },
},
});
@ -111,8 +112,8 @@ export default function LoginPage() {
};
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="flex h-[80vh] items-center justify-center">
<div className="w-full max-w-md space-y-6 rounded-lg p-8 shadow-md">
<div className="mb-6 space-y-2">
<Button
className="w-full"
@ -176,16 +177,46 @@ export default function LoginPage() {
</FormItem>
)}
/>
<div className="flex w-full space-x-4 mt-6 mb-6">
<FormField
control={form.control}
name="agreeToTerms"
render={({ field }) => (
<FormItem className="mt-4 flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
I agree to the{" "}
<Link href="/terms-of-service" className="underline">
Terms of Service
</Link>{" "}
and{" "}
<Link
href="https://www.notion.so/auto-gpt/Privacy-Policy-ab11c9c20dbd4de1a15dcffe84d77984"
className="underline"
>
Privacy Policy
</Link>
</FormLabel>
<FormMessage />
</div>
</FormItem>
)}
/>
<div className="mb-6 mt-6 flex w-full space-x-4">
<Button
className="w-1/2 flex justify-center"
className="flex w-1/2 justify-center"
type="submit"
disabled={isLoading}
>
Log in
</Button>
<Button
className="w-1/2 flex justify-center"
className="flex w-1/2 justify-center"
variant="outline"
type="button"
onClick={form.handleSubmit(onSignup)}
@ -195,10 +226,7 @@ export default function LoginPage() {
</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>
<p className="text-sm text-red-500">{feedback}</p>
</Form>
</div>
</div>

View File

@ -2,7 +2,7 @@ import { Suspense } from "react";
import { notFound } from "next/navigation";
import MarketplaceAPI from "@/lib/marketplace-api";
import { AgentDetailResponse } from "@/lib/marketplace-api";
import AgentDetailContent from "@/components/AgentDetailContent";
import AgentDetailContent from "@/components/marketplace/AgentDetailContent";
async function getAgentDetails(id: string): Promise<AgentDetailResponse> {
const apiUrl =

View File

@ -1,6 +1,7 @@
"use client";
import React, { useEffect, useMemo, useState, useCallback } from "react";
import { useRouter } from "next/navigation";
import Image from "next/image";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import MarketplaceAPI, {
@ -8,7 +9,13 @@ import MarketplaceAPI, {
AgentListResponse,
AgentWithRank,
} from "@/lib/marketplace-api";
import { ChevronLeft, ChevronRight, Search, Star } from "lucide-react";
import {
ChevronLeft,
ChevronRight,
PlusCircle,
Search,
Star,
} from "lucide-react";
// Utility Functions
function debounce<T extends (...args: any[]) => any>(
@ -26,44 +33,61 @@ function debounce<T extends (...args: any[]) => any>(
type Agent = AgentResponse | AgentWithRank;
// Components
const HeroSection: React.FC = () => (
<div className="relative bg-indigo-600 py-6">
<div className="absolute inset-0 z-0">
<img
className="w-full h-full object-cover opacity-20"
src="https://images.unsplash.com/photo-1562408590-e32931084e23?auto=format&fit=crop&w=2070&q=80"
alt="Marketplace background"
/>
<div
className="absolute inset-0 bg-indigo-600 mix-blend-multiply"
aria-hidden="true"
></div>
const HeroSection: React.FC = () => {
const router = useRouter();
return (
<div className="relative bg-indigo-600 py-6">
<div className="absolute inset-0 z-0">
<Image
src="https://images.unsplash.com/photo-1562408590-e32931084e23?auto=format&fit=crop&w=2070&q=80"
alt="Marketplace background"
layout="fill"
objectFit="cover"
quality={75}
priority
className="opacity-20"
/>
<div
className="absolute inset-0 bg-indigo-600 mix-blend-multiply"
aria-hidden="true"
></div>
</div>
<div className="relative mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6 lg:px-8">
<div>
<h1 className="text-2xl font-extrabold tracking-tight text-white sm:text-3xl lg:text-4xl">
AutoGPT Marketplace
</h1>
<p className="mt-2 max-w-3xl text-sm text-indigo-100 sm:text-base">
Discover and share proven AI Agents to supercharge your business.
</p>
</div>
<Button
onClick={() => router.push("/marketplace/submit")}
className="flex items-center bg-white text-indigo-600 hover:bg-indigo-50"
>
<PlusCircle className="mr-2 h-4 w-4" />
Submit Agent
</Button>
</div>
</div>
<div className="relative max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl font-extrabold tracking-tight text-white sm:text-3xl lg:text-4xl">
AutoGPT Marketplace
</h1>
<p className="mt-2 max-w-3xl text-sm sm:text-base text-indigo-100">
Discover and share proven AI Agents to supercharge your business.
</p>
</div>
</div>
);
);
};
const SearchInput: React.FC<{
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}> = ({ value, onChange }) => (
<div className="mb-8 relative">
<div className="relative mb-8">
<Input
placeholder="Search agents..."
type="text"
className="w-full pl-10 pr-4 py-2 rounded-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
className="w-full rounded-full border-gray-300 py-2 pl-10 pr-4 focus:border-indigo-500 focus:ring-indigo-500"
value={value}
onChange={onChange}
/>
<Search
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
className="absolute left-3 top-1/2 -translate-y-1/2 transform text-gray-400"
size={20}
/>
</div>
@ -81,24 +105,24 @@ const AgentCard: React.FC<{ agent: Agent; featured?: boolean }> = ({
return (
<div
className={`flex flex-col justify-between p-6 cursor-pointer hover:bg-gray-50 transition-colors duration-200 rounded-lg border ${featured ? "border-indigo-500 shadow-md" : "border-gray-200"}`}
className={`flex cursor-pointer flex-col justify-between rounded-lg border p-6 transition-colors duration-200 hover:bg-gray-50 ${featured ? "border-indigo-500 shadow-md" : "border-gray-200"}`}
onClick={handleClick}
>
<div>
<div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-semibold text-gray-900 truncate">
<div className="mb-2 flex items-center justify-between">
<h3 className="truncate text-lg font-semibold text-gray-900">
{agent.name}
</h3>
{featured && <Star className="text-indigo-500" size={20} />}
</div>
<p className="text-sm text-gray-500 line-clamp-2 mb-4">
<p className="mb-4 line-clamp-2 text-sm text-gray-500">
{agent.description}
</p>
<div className="text-xs text-gray-400 mb-2">
<div className="mb-2 text-xs text-gray-400">
Categories: {agent.categories.join(", ")}
</div>
</div>
<div className="flex justify-between items-end">
<div className="flex items-end justify-between">
<div className="text-xs text-gray-400">
Updated {new Date(agent.updatedAt).toLocaleDateString()}
</div>
@ -119,8 +143,8 @@ const AgentGrid: React.FC<{
featured?: boolean;
}> = ({ agents, title, featured = false }) => (
<div className="mb-12">
<h2 className="text-2xl font-bold text-gray-900 mb-4">{title}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<h2 className="mb-4 text-2xl font-bold text-gray-900">{title}</h2>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{agents.map((agent) => (
<AgentCard agent={agent} key={agent.id} featured={featured} />
))}
@ -134,11 +158,11 @@ const Pagination: React.FC<{
onPrevPage: () => void;
onNextPage: () => void;
}> = ({ page, totalPages, onPrevPage, onNextPage }) => (
<div className="flex justify-between items-center mt-8">
<div className="mt-8 flex items-center justify-between">
<Button
onClick={onPrevPage}
disabled={page === 1}
className="flex items-center space-x-2 px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
className="flex items-center space-x-2 rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50"
>
<ChevronLeft size={16} />
<span>Previous</span>
@ -149,7 +173,7 @@ const Pagination: React.FC<{
<Button
onClick={onNextPage}
disabled={page === totalPages}
className="flex items-center space-x-2 px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
className="flex items-center space-x-2 rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50"
>
<span>Next</span>
<ChevronRight size={16} />
@ -248,20 +272,20 @@ const Marketplace: React.FC = () => {
};
return (
<div className="bg-gray-50 min-h-screen">
<div className="min-h-screen bg-gray-50">
<HeroSection />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
<SearchInput value={searchValue} onChange={handleInputChange} />
{isLoading ? (
<div className="text-center py-12">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
<div className="py-12 text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-b-2 border-gray-900"></div>
<p className="mt-2 text-gray-600">Loading agents...</p>
</div>
) : searchValue ? (
searchResults.length > 0 ? (
<AgentGrid agents={searchResults} title="Search Results" />
) : (
<div className="text-center py-12">
<div className="py-12 text-center">
<p className="text-gray-600">
No agents found matching your search criteria.
</p>

View File

@ -0,0 +1,408 @@
"use client";
import React, { useState, useEffect, useMemo } from "react";
import { useRouter } from "next/navigation";
import { useForm, Controller } from "react-hook-form";
import MarketplaceAPI from "@/lib/marketplace-api";
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
import { Checkbox } from "@/components/ui/checkbox";
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorInput,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from "@/components/ui/multiselect";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
type FormData = {
name: string;
description: string;
author: string;
keywords: string[];
categories: string[];
agreeToTerms: boolean;
selectedAgentId: string;
};
const SubmitPage: React.FC = () => {
const router = useRouter();
const {
control,
handleSubmit,
watch,
setValue,
formState: { errors },
} = useForm<FormData>({
defaultValues: {
selectedAgentId: "", // Initialize with an empty string
name: "",
description: "",
author: "",
keywords: [],
categories: [],
agreeToTerms: false,
},
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const [userAgents, setUserAgents] = useState<
Array<{ id: string; name: string; version: number }>
>([]);
const [selectedAgentGraph, setSelectedAgentGraph] = useState<any>(null);
const selectedAgentId = watch("selectedAgentId");
useEffect(() => {
const fetchUserAgents = async () => {
const api = new AutoGPTServerAPI();
const agents = await api.listGraphs();
console.log(agents);
setUserAgents(
agents.map((agent) => ({
id: agent.id,
name: agent.name || `Agent (${agent.id})`,
version: agent.version,
})),
);
};
fetchUserAgents();
}, []);
useEffect(() => {
const fetchAgentGraph = async () => {
if (selectedAgentId) {
const api = new AutoGPTServerAPI();
const graph = await api.getGraph(selectedAgentId);
setSelectedAgentGraph(graph);
setValue("name", graph.name);
setValue("description", graph.description);
}
};
fetchAgentGraph();
}, [selectedAgentId, setValue]);
const onSubmit = async (data: FormData) => {
setIsSubmitting(true);
setSubmitError(null);
if (!data.agreeToTerms) {
throw new Error("You must agree to the terms of service");
}
try {
if (!selectedAgentGraph) {
throw new Error("Please select an agent");
}
const api = new MarketplaceAPI();
await api.submitAgent(
{
...selectedAgentGraph,
name: data.name,
description: data.description,
},
data.author,
data.keywords,
data.categories,
);
router.push("/marketplace?submission=success");
} catch (error) {
console.error("Submission error:", error);
setSubmitError(
error instanceof Error ? error.message : "An unknown error occurred",
);
} finally {
setIsSubmitting(false);
}
};
return (
<div className="container mx-auto px-4 py-8">
<h1 className="mb-6 text-3xl font-bold">Submit Your Agent</h1>
<Card className="p-6">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-4">
<Controller
name="selectedAgentId"
control={control}
rules={{ required: "Please select an agent" }}
render={({ field }) => (
<div>
<label
htmlFor={field.name}
className="block text-sm font-medium text-gray-700"
>
Select Agent
</label>
<Select
onValueChange={field.onChange}
value={field.value || ""}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an agent" />
</SelectTrigger>
<SelectContent>
{userAgents.map((agent) => (
<SelectItem key={agent.id} value={agent.id}>
{agent.name} (v{agent.version})
</SelectItem>
))}
</SelectContent>
</Select>
{errors.selectedAgentId && (
<p className="mt-1 text-sm text-red-600">
{errors.selectedAgentId.message}
</p>
)}
</div>
)}
/>
{/* {selectedAgentGraph && (
<div className="mt-4" style={{ height: "600px" }}>
<ReactFlow
nodes={nodes}
edges={edges}
fitView
attributionPosition="bottom-left"
nodesConnectable={false}
nodesDraggable={false}
zoomOnScroll={false}
panOnScroll={false}
elementsSelectable={false}
>
<Controls showInteractive={false} />
<Background />
</ReactFlow>
</div>
)} */}
<Controller
name="name"
control={control}
rules={{ required: "Name is required" }}
render={({ field }) => (
<div>
<label
htmlFor={field.name}
className="block text-sm font-medium text-gray-700"
>
Agent Name
</label>
<Input
id={field.name}
placeholder="Enter your agent's name"
{...field}
/>
{errors.name && (
<p className="mt-1 text-sm text-red-600">
{errors.name.message}
</p>
)}
</div>
)}
/>
<Controller
name="description"
control={control}
rules={{ required: "Description is required" }}
render={({ field }) => (
<div>
<label
htmlFor={field.name}
className="block text-sm font-medium text-gray-700"
>
Description
</label>
<Textarea
id={field.name}
placeholder="Describe your agent"
{...field}
/>
{errors.description && (
<p className="mt-1 text-sm text-red-600">
{errors.description.message}
</p>
)}
</div>
)}
/>
<Controller
name="author"
control={control}
rules={{ required: "Author is required" }}
render={({ field }) => (
<div>
<label
htmlFor={field.name}
className="block text-sm font-medium text-gray-700"
>
Author
</label>
<Input
id={field.name}
placeholder="Your name or username"
{...field}
/>
{errors.author && (
<p className="mt-1 text-sm text-red-600">
{errors.author.message}
</p>
)}
</div>
)}
/>
<Controller
name="keywords"
control={control}
rules={{ required: "At least one keyword is required" }}
render={({ field }) => (
<div>
<label
htmlFor={field.name}
className="block text-sm font-medium text-gray-700"
>
Keywords
</label>
<MultiSelector
values={field.value || []}
onValuesChange={field.onChange}
>
<MultiSelectorTrigger>
<MultiSelectorInput placeholder="Add keywords" />
</MultiSelectorTrigger>
<MultiSelectorContent>
<MultiSelectorList>
<MultiSelectorItem value="keyword1">
Keyword 1
</MultiSelectorItem>
<MultiSelectorItem value="keyword2">
Keyword 2
</MultiSelectorItem>
{/* Add more predefined keywords as needed */}
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
{errors.keywords && (
<p className="mt-1 text-sm text-red-600">
{errors.keywords.message}
</p>
)}
</div>
)}
/>
<Controller
name="categories"
control={control}
rules={{ required: "At least one category is required" }}
render={({ field }) => (
<div>
<label
htmlFor={field.name}
className="block text-sm font-medium text-gray-700"
>
Categories
</label>
<MultiSelector
values={field.value || []}
onValuesChange={field.onChange}
>
<MultiSelectorTrigger>
<MultiSelectorInput placeholder="Select categories" />
</MultiSelectorTrigger>
<MultiSelectorContent>
<MultiSelectorList>
<MultiSelectorItem value="productivity">
Productivity
</MultiSelectorItem>
<MultiSelectorItem value="entertainment">
Entertainment
</MultiSelectorItem>
<MultiSelectorItem value="education">
Education
</MultiSelectorItem>
<MultiSelectorItem value="business">
Business
</MultiSelectorItem>
<MultiSelectorItem value="other">
Other
</MultiSelectorItem>
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
{errors.categories && (
<p className="mt-1 text-sm text-red-600">
{errors.categories.message}
</p>
)}
</div>
)}
/>
<Controller
name="agreeToTerms"
control={control}
rules={{ required: "You must agree to the terms of service" }}
render={({ field }) => (
<div className="flex items-center space-x-2">
<Checkbox
id="agreeToTerms"
checked={field.value}
onCheckedChange={field.onChange}
/>
<label
htmlFor="agreeToTerms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
I agree to the{" "}
<a href="/terms" className="text-blue-500 hover:underline">
terms of service
</a>
</label>
</div>
)}
/>
{errors.agreeToTerms && (
<p className="mt-1 text-sm text-red-600">
{errors.agreeToTerms.message}
</p>
)}
{submitError && (
<Alert variant="destructive">
<AlertTitle>Submission Failed</AlertTitle>
<AlertDescription>{submitError}</AlertDescription>
</Alert>
)}
<Button type="submit" className="w-full" disabled={isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit Agent"}
</Button>
</div>
</form>
</Card>
</div>
);
};
export default SubmitPage;

View File

@ -1,5 +1,5 @@
"use client";
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import AutoGPTServerAPI, {
GraphMeta,
@ -22,61 +22,64 @@ const Monitor = () => {
const [selectedFlow, setSelectedFlow] = useState<GraphMeta | null>(null);
const [selectedRun, setSelectedRun] = useState<FlowRun | null>(null);
const api = new AutoGPTServerAPI();
const api = useMemo(() => new AutoGPTServerAPI(), []);
useEffect(() => fetchFlowsAndRuns(), []);
const refreshFlowRuns = useCallback(
(flowID: string) => {
// Fetch flow run IDs
api.listGraphRunIDs(flowID).then((runIDs) =>
runIDs.map((runID) => {
let run;
if (
(run = flowRuns.find((fr) => fr.id == runID)) &&
!["waiting", "running"].includes(run.status)
) {
return;
}
// Fetch flow run
api.getGraphExecutionInfo(flowID, runID).then((execInfo) =>
setFlowRuns((flowRuns) => {
if (execInfo.length == 0) return flowRuns;
const flowRunIndex = flowRuns.findIndex((fr) => fr.id == runID);
const flowRun = flowRunFromNodeExecutionResults(execInfo);
if (flowRunIndex > -1) {
flowRuns.splice(flowRunIndex, 1, flowRun);
} else {
flowRuns.push(flowRun);
}
return [...flowRuns];
}),
);
}),
);
},
[api, flowRuns],
);
const fetchFlowsAndRuns = useCallback(() => {
api.listGraphs().then((flows) => {
setFlows(flows);
flows.map((flow) => refreshFlowRuns(flow.id));
});
}, [api, refreshFlowRuns]);
useEffect(() => fetchFlowsAndRuns(), [fetchFlowsAndRuns]);
useEffect(() => {
const intervalId = setInterval(
() => flows.map((f) => refreshFlowRuns(f.id)),
5000,
);
return () => clearInterval(intervalId);
}, []);
function fetchFlowsAndRuns() {
api.listGraphs().then((flows) => {
setFlows(flows);
flows.map((flow) => refreshFlowRuns(flow.id));
});
}
function refreshFlowRuns(flowID: string) {
// Fetch flow run IDs
api.listGraphRunIDs(flowID).then((runIDs) =>
runIDs.map((runID) => {
let run;
if (
(run = flowRuns.find((fr) => fr.id == runID)) &&
!["waiting", "running"].includes(run.status)
) {
return;
}
// Fetch flow run
api.getGraphExecutionInfo(flowID, runID).then((execInfo) =>
setFlowRuns((flowRuns) => {
if (execInfo.length == 0) return flowRuns;
const flowRunIndex = flowRuns.findIndex((fr) => fr.id == runID);
const flowRun = flowRunFromNodeExecutionResults(execInfo);
if (flowRunIndex > -1) {
flowRuns.splice(flowRunIndex, 1, flowRun);
} else {
flowRuns.push(flowRun);
}
return [...flowRuns];
}),
);
}),
);
}
}, [flows, refreshFlowRuns]);
const column1 = "md:col-span-2 xl:col-span-3 xxl:col-span-2";
const column2 = "md:col-span-3 lg:col-span-2 xl:col-span-3 space-y-4";
const column3 = "col-span-full xl:col-span-4 xxl:col-span-5";
return (
<div className="grid grid-cols-1 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10 gap-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10">
<AgentFlowList
className={column1}
flows={flows}
@ -90,10 +93,11 @@ const Monitor = () => {
<FlowRunsList
className={column2}
flows={flows}
runs={(selectedFlow
? flowRuns.filter((v) => v.graphID == selectedFlow.id)
: flowRuns
).toSorted((a, b) => Number(a.startTime) - Number(b.startTime))}
runs={[
...(selectedFlow
? flowRuns.filter((v) => v.graphID == selectedFlow.id)
: flowRuns),
].sort((a, b) => Number(a.startTime) - Number(b.startTime))}
selectedRun={selectedRun}
onSelectRun={(r) => setSelectedRun(r.id == selectedRun?.id ? null : r)}
/>

View File

@ -13,7 +13,7 @@ export default function PrivatePage() {
if (isLoading) {
return (
<div className="flex justify-center items-center h-[80vh]">
<div className="flex h-[80vh] items-center justify-center">
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
</div>
);

View File

@ -0,0 +1,9 @@
// app/unauthorized/page.tsx
export default function Unauthorized() {
return (
<div>
<h1>Unauthorized Access</h1>
<p>You do not have permission to view this page.</p>
</div>
);
}

View File

@ -3,7 +3,7 @@ import {
ConnectionLineComponentProps,
getBezierPath,
Position,
} from "reactflow";
} from "@xyflow/react";
const ConnectionLine: React.FC<ConnectionLineComponentProps> = ({
fromPosition,

View File

@ -1,89 +1,197 @@
import React, { FC, memo, useMemo, useState } from "react";
import React, { useCallback, useContext, useEffect, useState } from "react";
import {
BaseEdge,
EdgeLabelRenderer,
EdgeProps,
getBezierPath,
useReactFlow,
XYPosition,
} from "reactflow";
Edge,
Node,
} from "@xyflow/react";
import "./customedge.css";
import { X } from "lucide-react";
import { useBezierPath } from "@/hooks/useBezierPath";
import { FlowContext } from "./Flow";
export type CustomEdgeData = {
edgeColor: string;
sourcePos?: XYPosition;
isStatic?: boolean;
beadUp?: number;
beadDown?: number;
beadData?: any[];
};
const CustomEdgeFC: FC<EdgeProps<CustomEdgeData>> = ({
type Bead = {
t: number;
targetT: number;
startTime: number;
};
export type CustomEdge = Edge<CustomEdgeData, "custom">;
export function CustomEdge({
id,
data,
selected,
source,
sourcePosition,
sourceX,
sourceY,
target,
targetPosition,
targetX,
targetY,
markerEnd,
}) => {
}: EdgeProps<CustomEdge>) {
const [isHovered, setIsHovered] = useState(false);
const { setEdges } = useReactFlow();
const onEdgeRemoveClick = () => {
setEdges((edges) => edges.filter((edge) => edge.id !== id));
const [beads, setBeads] = useState<{
beads: Bead[];
created: number;
destroyed: number;
}>({ beads: [], created: 0, destroyed: 0 });
const { svgPath, length, getPointForT, getTForDistance } = useBezierPath(
sourceX - 5,
sourceY,
targetX + 3,
targetY,
);
const { deleteElements } = useReactFlow<Node, CustomEdge>();
const { visualizeBeads } = useContext(FlowContext) ?? {
visualizeBeads: "no",
};
const [path, labelX, labelY] = getBezierPath({
sourceX: sourceX - 5,
sourceY,
sourcePosition,
targetX: targetX + 4,
targetY,
targetPosition,
});
const onEdgeRemoveClick = () => {
deleteElements({ edges: [{ id }] });
};
// Calculate y difference between source and source node, to adjust self-loop edge
const yDifference = useMemo(
() => sourceY - (data?.sourcePos?.y || 0),
[data?.sourcePos?.y],
const animationDuration = 500; // Duration in milliseconds for bead to travel the curve
const beadDiameter = 12;
const deltaTime = 16;
const setTargetPositions = useCallback(
(beads: Bead[]) => {
const distanceBetween = Math.min(
(length - beadDiameter) / (beads.length + 1),
beadDiameter,
);
return beads.map((bead, index) => {
const distanceFromEnd = beadDiameter * 1.35;
const targetPosition = distanceBetween * index + distanceFromEnd;
const t = getTForDistance(-targetPosition);
return {
...bead,
t: visualizeBeads === "animate" ? bead.t : t,
targetT: t,
} as Bead;
});
},
[getTForDistance, length, visualizeBeads],
);
// 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;
useEffect(() => {
if (data?.beadUp === 0 && data?.beadDown === 0) {
setBeads({ beads: [], created: 0, destroyed: 0 });
return;
}
console.table({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
path,
labelX,
labelY,
});
const beadUp = data?.beadUp!;
// Add beads
setBeads(({ beads, created, destroyed }) => {
const newBeads = [];
for (let i = 0; i < beadUp - created; i++) {
newBeads.push({ t: 0, targetT: 0, startTime: Date.now() });
}
const b = setTargetPositions([...beads, ...newBeads]);
return { beads: b, created: beadUp, destroyed };
});
// Remove beads if not animating
if (visualizeBeads !== "animate") {
setBeads(({ beads, created, destroyed }) => {
let destroyedCount = 0;
const newBeads = beads
.map((bead) => ({ ...bead }))
.filter((bead, index) => {
const beadDown = data?.beadDown!;
// Remove always one less bead in case of static edge, so it stays at the connection point
const removeCount = beadDown - destroyed - (data?.isStatic ? 1 : 0);
if (bead.t >= bead.targetT && index < removeCount) {
destroyedCount++;
return false;
}
return true;
});
return {
beads: setTargetPositions(newBeads),
created,
destroyed: destroyed + destroyedCount,
};
});
return;
}
// Animate and remove beads
const interval = setInterval(() => {
setBeads(({ beads, created, destroyed }) => {
let destroyedCount = 0;
const newBeads = beads
.map((bead) => {
const progressIncrement = deltaTime / animationDuration;
const t = Math.min(
bead.t + bead.targetT * progressIncrement,
bead.targetT,
);
return {
...bead,
t,
};
})
.filter((bead, index) => {
const beadDown = data?.beadDown!;
// Remove always one less bead in case of static edge, so it stays at the connection point
const removeCount = beadDown - destroyed - (data?.isStatic ? 1 : 0);
if (bead.t >= bead.targetT && index < removeCount) {
destroyedCount++;
return false;
}
return true;
});
return {
beads: setTargetPositions(newBeads),
created,
destroyed: destroyed + destroyedCount,
};
});
}, deltaTime);
return () => clearInterval(interval);
}, [data, setTargetPositions, visualizeBeads]);
const middle = getPointForT(0.5);
return (
<>
<BaseEdge
path={edgePath}
path={svgPath}
markerEnd={markerEnd}
style={{
strokeWidth: isHovered ? 3 : 2,
strokeWidth: (isHovered ? 3 : 2) + (data?.isStatic ? 0.5 : 0),
stroke:
(data?.edgeColor ?? "#555555") +
(selected || isHovered ? "" : "80"),
strokeDasharray: data?.isStatic ? "5 3" : "0",
}}
/>
<path
d={edgePath}
d={svgPath}
fill="none"
strokeOpacity={0}
strokeWidth={20}
@ -95,7 +203,7 @@ const CustomEdgeFC: FC<EdgeProps<CustomEdgeData>> = ({
<div
style={{
position: "absolute",
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
transform: `translate(-50%, -50%) translate(${middle.x}px,${middle.y}px)`,
pointerEvents: "all",
}}
className="edge-label-renderer"
@ -110,8 +218,18 @@ const CustomEdgeFC: FC<EdgeProps<CustomEdgeData>> = ({
</button>
</div>
</EdgeLabelRenderer>
{beads.beads.map((bead, index) => {
const pos = getPointForT(bead.t);
return (
<circle
key={index}
cx={pos.x}
cy={pos.y}
r={beadDiameter / 2} // Bead radius
fill={data?.edgeColor ?? "#555555"}
/>
);
})}
</>
);
};
export const CustomEdge = memo(CustomEdgeFC);
}

View File

@ -1,89 +1,121 @@
import React, {
useState,
useEffect,
FC,
memo,
useCallback,
useRef,
useContext,
} from "react";
import { NodeProps, useReactFlow } from "reactflow";
import "reactflow/dist/style.css";
import { NodeProps, useReactFlow, Node, Edge } from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import "./customnode.css";
import InputModalComponent from "./InputModalComponent";
import OutputModalComponent from "./OutputModalComponent";
import {
BlockIORootSchema,
BlockIOStringSubSchema,
Category,
NodeExecutionResult,
BlockUIType,
} from "@/lib/autogpt-server-api/types";
import { beautifyString, setNestedProperty } from "@/lib/utils";
import { beautifyString, cn, setNestedProperty } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Copy, Trash2 } from "lucide-react";
import { history } from "./history";
import NodeHandle from "./NodeHandle";
import { CustomEdgeData } from "./CustomEdge";
import { NodeGenericInputField } from "./node-input-components";
import {
NodeGenericInputField,
NodeTextBoxInput,
} from "./node-input-components";
import SchemaTooltip from "./SchemaTooltip";
import { getPrimaryCategoryColor } from "@/lib/utils";
import { FlowContext } from "./Flow";
import { Badge } from "./ui/badge";
import DataTable from "./DataTable";
type ParsedKey = { key: string; index?: number };
export type ConnectionData = Array<{
edge_id: string;
source: string;
sourceHandle: string;
target: string;
targetHandle: string;
}>;
export type CustomNodeData = {
blockType: string;
title: string;
description: string;
categories: Category[];
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: ConnectionData;
isOutputOpen: boolean;
status?: NodeExecutionResult["status"];
output_data?: NodeExecutionResult["output_data"];
/** executionResults contains outputs across multiple executions
* with the last element being the most recent output */
executionResults?: {
execId: string;
data: NodeExecutionResult["output_data"];
}[];
block_id: string;
backend_id?: string;
errors?: { [key: string]: string | null };
setErrors: (errors: { [key: string]: string | null }) => void;
setIsAnyModalOpen?: (isOpen: boolean) => void;
errors?: { [key: string]: string };
isOutputStatic?: boolean;
uiType: BlockUIType;
};
const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
export type CustomNode = Node<CustomNodeData, "custom">;
export function CustomNode({ data, id, width, height }: NodeProps<CustomNode>) {
const [isOutputOpen, setIsOutputOpen] = useState(data.isOutputOpen || false);
const [isAdvancedOpen, setIsAdvancedOpen] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [activeKey, setActiveKey] = useState<string | null>(null);
const [modalValue, setModalValue] = useState<string>("");
const [inputModalValue, setInputModalValue] = useState<string>("");
const [isOutputModalOpen, setIsOutputModalOpen] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const { getNode, setNodes, getEdges, setEdges } = useReactFlow<
CustomNodeData,
CustomEdgeData
const { updateNodeData, deleteElements, addNodes, getNode } = useReactFlow<
CustomNode,
Edge
>();
const outputDataRef = useRef<HTMLDivElement>(null);
const isInitialSetup = useRef(true);
const flowContext = useContext(FlowContext);
if (!flowContext) {
throw new Error("FlowContext consumer must be inside FlowEditor component");
}
const { setIsAnyModalOpen, getNextNodeId } = flowContext;
useEffect(() => {
if (data.output_data || data.status) {
if (data.executionResults || data.status) {
setIsOutputOpen(true);
}
}, [data.output_data, data.status]);
}, [data.executionResults, data.status]);
useEffect(() => {
setIsOutputOpen(data.isOutputOpen);
}, [data.isOutputOpen]);
useEffect(() => {
data.setIsAnyModalOpen?.(isModalOpen || isOutputModalOpen);
}, [isModalOpen, isOutputModalOpen, data]);
setIsAnyModalOpen?.(isModalOpen || isOutputModalOpen);
}, [isModalOpen, isOutputModalOpen, data, setIsAnyModalOpen]);
useEffect(() => {
isInitialSetup.current = false;
}, []);
const setHardcodedValues = (values: any) => {
updateNodeData(id, { hardcodedValues: values });
};
const setErrors = (errors: { [key: string]: string }) => {
updateNodeData(id, { errors });
};
const toggleOutput = (checked: boolean) => {
setIsOutputOpen(checked);
};
@ -92,14 +124,16 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
setIsAdvancedOpen(checked);
};
const hasOptionalFields =
data.inputSchema &&
Object.keys(data.inputSchema.properties).some((key) => {
return !data.inputSchema.required?.includes(key);
});
const generateOutputHandles = (schema: BlockIORootSchema) => {
if (!schema?.properties) return null;
const generateOutputHandles = (
schema: BlockIORootSchema,
nodeType: BlockUIType,
) => {
if (
!schema?.properties ||
nodeType === BlockUIType.OUTPUT ||
nodeType === BlockUIType.NOTE
)
return null;
const keys = Object.keys(schema.properties);
return keys.map((key) => (
<div key={key}>
@ -113,6 +147,137 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
));
};
const generateInputHandles = (
schema: BlockIORootSchema,
nodeType: BlockUIType,
) => {
if (!schema?.properties) return null;
let keys = Object.entries(schema.properties);
switch (nodeType) {
case BlockUIType.INPUT:
// For INPUT blocks, dont include connection handles
return keys.map(([propKey, propSchema]) => {
const isRequired = data.inputSchema.required?.includes(propKey);
const isConnected = isHandleConnected(propKey);
const isAdvanced = propSchema.advanced;
return (
(isRequired || isAdvancedOpen || !isAdvanced) && (
<div key={propKey}>
<span className="text-m green -mb-1 text-gray-900">
{propSchema.title || beautifyString(propKey)}
</span>
<div key={propKey} onMouseOver={() => {}}>
{!isConnected && (
<NodeGenericInputField
className="mb-2 mt-1"
propKey={propKey}
propSchema={propSchema}
currentValue={getValue(propKey)}
connections={data.connections}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
errors={data.errors ?? {}}
displayName={propSchema.title || beautifyString(propKey)}
/>
)}
</div>
</div>
)
);
});
case BlockUIType.NOTE:
// For NOTE blocks, don't render any input handles
const [noteKey, noteSchema] = keys[0];
return (
<div key={noteKey}>
<NodeTextBoxInput
className=""
selfKey={noteKey}
schema={noteSchema as BlockIOStringSubSchema}
value={getValue(noteKey)}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
error={data.errors?.[noteKey] ?? ""}
displayName={noteSchema.title || beautifyString(noteKey)}
/>
</div>
);
case BlockUIType.OUTPUT:
// For OUTPUT blocks, only show the 'value' property
return keys.map(([propKey, propSchema]) => {
const isRequired = data.inputSchema.required?.includes(propKey);
const isConnected = isHandleConnected(propKey);
const isAdvanced = propSchema.advanced;
return (
(isRequired || isAdvancedOpen || !isAdvanced) && (
<div key={propKey} onMouseOver={() => {}}>
{propKey !== "value" ? (
<span className="text-m green -mb-1 text-gray-900">
{propSchema.title || beautifyString(propKey)}
</span>
) : (
<NodeHandle
keyName={propKey}
isConnected={isConnected}
isRequired={isRequired}
schema={propSchema}
side="left"
/>
)}
{!isConnected && (
<NodeGenericInputField
className="mb-2 mt-1"
propKey={propKey}
propSchema={propSchema}
currentValue={getValue(propKey)}
connections={data.connections}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
errors={data.errors ?? {}}
displayName={propSchema.title || beautifyString(propKey)}
/>
)}
</div>
)
);
});
default:
return keys.map(([propKey, propSchema]) => {
const isRequired = data.inputSchema.required?.includes(propKey);
const isConnected = isHandleConnected(propKey);
const isAdvanced = propSchema.advanced;
return (
(isRequired || isAdvancedOpen || isConnected || !isAdvanced) && (
<div key={propKey} onMouseOver={() => {}}>
<NodeHandle
keyName={propKey}
isConnected={isConnected}
isRequired={isRequired}
schema={propSchema}
side="left"
/>
{!isConnected && (
<NodeGenericInputField
className="mb-2 mt-1"
propKey={propKey}
propSchema={propSchema}
currentValue={getValue(propKey)}
connections={data.connections}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
errors={data.errors ?? {}}
displayName={propSchema.title || beautifyString(propKey)}
/>
)}
</div>
)
);
});
}
};
const handleInputChange = (path: string, value: any) => {
const keys = parseKeys(path);
const newValues = JSON.parse(JSON.stringify(data.hardcodedValues));
@ -138,46 +303,47 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
current[lastKey.key] = value;
}
console.log(`Updating hardcoded values for node ${id}:`, newValues);
// 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),
undo: () => setHardcodedValues(data.hardcodedValues),
redo: () => setHardcodedValues(newValues),
});
}
data.setHardcodedValues(newValues);
setHardcodedValues(newValues);
const errors = data.errors || {};
// Remove error with the same key
setNestedProperty(errors, path, null);
data.setErrors({ ...errors });
setErrors({ ...errors });
};
// Helper function to parse keys with array indices
//TODO move to utils
const parseKeys = (key: string): ParsedKey[] => {
const regex = /(\w+)|\[(\d+)\]/g;
const splits = key.split(/_@_|_#_|_\$_|\./);
const keys: ParsedKey[] = [];
let match;
let currentKey: string | null = null;
while ((match = regex.exec(key)) !== null) {
if (match[1]) {
splits.forEach((split) => {
const isInteger = /^\d+$/.test(split);
if (!isInteger) {
if (currentKey !== null) {
keys.push({ key: currentKey });
}
currentKey = match[1];
} else if (match[2]) {
currentKey = split;
} else {
if (currentKey !== null) {
keys.push({ key: currentKey, index: parseInt(match[2], 10) });
keys.push({ key: currentKey, index: parseInt(split, 10) });
currentKey = null;
} else {
throw new Error("Invalid key format: array index without a key");
}
}
}
});
if (currentKey !== null) {
keys.push({ key: currentKey });
@ -220,7 +386,7 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
console.log(`Opening modal for key: ${key}`);
setActiveKey(key);
const value = getValue(key);
setModalValue(
setInputModalValue(
typeof value === "object" ? JSON.stringify(value, null, 2) : value,
);
setIsModalOpen(true);
@ -241,19 +407,6 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
const handleOutputClick = () => {
setIsOutputModalOpen(true);
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
);
};
const handleHovered = () => {
@ -267,60 +420,124 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
const deleteNode = useCallback(() => {
console.log("Deleting node:", id);
// Get all edges connected to this node
const connectedEdges = getEdges().filter(
(edge) => edge.source === id || edge.target === id,
);
// For each connected edge, update the connected node's state
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;
}),
);
}
});
// 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),
);
}, [id, setNodes, setEdges, getNode, getEdges]);
// Remove the node
deleteElements({ nodes: [{ id }] });
}, [id, deleteElements]);
const copyNode = useCallback(() => {
// This is a placeholder function. The actual copy functionality
// will be implemented by another team member.
console.log("Copy node:", id);
}, [id]);
const newId = getNextNodeId();
const currentNode = getNode(id);
if (!currentNode) {
console.error("Cannot copy node: current node not found");
return;
}
const verticalOffset = height ?? 100;
const newNode: CustomNode = {
id: newId,
type: currentNode.type,
position: {
x: currentNode.position.x,
y: currentNode.position.y - verticalOffset - 20,
},
data: {
...data,
title: `${data.title} (Copy)`,
block_id: data.block_id,
connections: [],
isOutputOpen: false,
},
};
addNodes(newNode);
history.push({
type: "ADD_NODE",
payload: { node: newNode },
undo: () => deleteElements({ nodes: [{ id: newId }] }),
redo: () => addNodes(newNode),
});
}, [id, data, height, addNodes, deleteElements, getNode, getNextNodeId]);
const hasConfigErrors =
data.errors &&
Object.entries(data.errors).some(([_, value]) => value !== null);
const outputData = data.executionResults?.at(-1)?.data;
const hasOutputError =
typeof outputData === "object" &&
outputData !== null &&
"error" in outputData;
useEffect(() => {
if (hasConfigErrors) {
const filteredErrors = Object.fromEntries(
Object.entries(data.errors || {}).filter(
([_, value]) => value !== null,
),
);
console.error(
"Block configuration errors for",
data.title,
":",
filteredErrors,
);
}
if (hasOutputError) {
console.error(
"Block output contains error for",
data.title,
":",
outputData.error,
);
}
}, [hasConfigErrors, hasOutputError, data.errors, outputData, data.title]);
const blockClasses = [
"custom-node",
"dark-theme",
"rounded-xl",
"border",
"bg-white/[.9]",
"shadow-md",
]
.filter(Boolean)
.join(" ");
const errorClass =
hasConfigErrors || hasOutputError ? "border-red-500 border-2" : "";
const statusClass =
hasConfigErrors || hasOutputError
? "failed"
: (data.status?.toLowerCase() ?? "");
const hasAdvancedFields =
data.inputSchema &&
Object.entries(data.inputSchema.properties).some(([key, value]) => {
return (
value.advanced === true && !data.inputSchema.required?.includes(key)
);
});
return (
<div
className={`custom-node dark-theme ${data.status?.toLowerCase() ?? ""}`}
className={`${data.uiType === BlockUIType.NOTE ? "w-[300px]" : "w-[500px]"} ${blockClasses} ${errorClass} ${statusClass} ${data.uiType === BlockUIType.NOTE ? "bg-yellow-100" : "bg-white"}`}
onMouseEnter={handleHovered}
onMouseLeave={handleMouseLeave}
data-id={`custom-node-${id}`}
>
<div className="mb-2">
<div className="text-lg font-bold">
{beautifyString(data.blockType?.replace(/Block$/, "") || data.title)}
<div
className={`mb-2 p-3 ${data.uiType === BlockUIType.NOTE ? "bg-yellow-100" : getPrimaryCategoryColor(data.categories)} rounded-t-xl`}
>
<div className="flex items-center justify-between">
<div className="font-roboto p-3 text-lg font-semibold">
{beautifyString(
data.blockType?.replace(/Block$/, "") || data.title,
)}
</div>
<SchemaTooltip description={data.description} />
</div>
<div className="flex gap-[5px]">
{isHovered && (
@ -345,96 +562,80 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
)}
</div>
</div>
<div className="flex justify-between items-start gap-2">
{data.uiType !== BlockUIType.NOTE ? (
<div className="flex items-start justify-between p-3">
<div>
{data.inputSchema &&
generateInputHandles(data.inputSchema, data.uiType)}
</div>
<div className="flex-none">
{data.outputSchema &&
generateOutputHandles(data.outputSchema, data.uiType)}
</div>
</div>
) : (
<div>
{data.inputSchema &&
Object.entries(data.inputSchema.properties).map(
([propKey, propSchema]) => {
const isRequired = data.inputSchema.required?.includes(propKey);
const isConnected = isHandleConnected(propKey);
return (
(isRequired || isAdvancedOpen || isConnected) && (
<div key={propKey} onMouseOver={() => {}}>
<NodeHandle
keyName={propKey}
isConnected={isConnected}
isRequired={isRequired}
schema={propSchema}
side="left"
/>
{!isConnected && (
<NodeGenericInputField
className="mt-1 mb-2"
propKey={propKey}
propSchema={propSchema}
currentValue={getValue(propKey)}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
errors={data.errors ?? {}}
displayName={
propSchema.title || beautifyString(propKey)
}
/>
)}
</div>
)
);
},
)}
</div>
<div className="flex-none">
{data.outputSchema && generateOutputHandles(data.outputSchema)}
</div>
</div>
{isOutputOpen && (
<div className="node-output" onClick={handleOutputClick}>
<p>
<strong>Status:</strong>{" "}
{typeof data.status === "object"
? JSON.stringify(data.status)
: data.status || "N/A"}
</p>
<p>
<strong>Output Data:</strong>{" "}
{(() => {
const outputText =
typeof data.output_data === "object"
? JSON.stringify(data.output_data)
: data.output_data;
if (!outputText) return "No output data";
return outputText.length > 100
? `${outputText.slice(0, 100)}... Press To Read More`
: outputText;
})()}
</p>
generateInputHandles(data.inputSchema, data.uiType)}
</div>
)}
{isOutputOpen && data.uiType !== BlockUIType.NOTE && (
<div
data-id="latest-output"
className="nodrag m-3 break-words rounded-md border-[1.5px] p-2"
>
{(data.executionResults?.length ?? 0) > 0 ? (
<>
<DataTable
title="Latest Output"
truncateLongData
data={data.executionResults!.at(-1)?.data || {}}
/>
<div className="flex justify-end">
<Button variant="ghost" onClick={handleOutputClick}>
View More
</Button>
</div>
</>
) : (
<span>No outputs yet</span>
)}
</div>
)}
{data.uiType !== BlockUIType.NOTE && (
<div className="mt-2.5 flex items-center pb-4 pl-4">
<Switch checked={isOutputOpen} onCheckedChange={toggleOutput} />
<span className="m-1 mr-4">Output</span>
{hasAdvancedFields && (
<>
<Switch onCheckedChange={toggleAdvancedSettings} />
<span className="m-1">Advanced</span>
</>
)}
{data.status && (
<Badge
variant="outline"
data-id={`badge-${id}-${data.status}`}
className={cn(data.status.toLowerCase(), "ml-auto mr-5")}
>
{data.status}
</Badge>
)}
</div>
)}
<div className="flex items-center mt-2.5">
<Switch onCheckedChange={toggleOutput} />
<span className="m-1 mr-4">Output</span>
{hasOptionalFields && (
<>
<Switch onCheckedChange={toggleAdvancedSettings} />
<span className="m-1">Advanced</span>
</>
)}
</div>
<InputModalComponent
title={activeKey ? `Enter ${beautifyString(activeKey)}` : undefined}
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onSave={handleModalSave}
value={modalValue}
defaultValue={inputModalValue}
key={activeKey}
/>
<OutputModalComponent
isOpen={isOutputModalOpen}
onClose={() => setIsOutputModalOpen(false)}
value={modalValue}
executionResults={data.executionResults?.toReversed() || []}
/>
</div>
);
};
export default memo(CustomNode);
}

View File

@ -0,0 +1,92 @@
import { beautifyString } from "@/lib/utils";
import { Button } from "./ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "./ui/table";
import { Clipboard } from "lucide-react";
import { useToast } from "./ui/use-toast";
type DataTableProps = {
title?: string;
truncateLongData?: boolean;
data: { [key: string]: Array<any> };
};
export default function DataTable({
title,
truncateLongData,
data,
}: DataTableProps) {
const { toast } = useToast();
const maxChars = 100;
const copyData = (pin: string, data: string) => {
navigator.clipboard.writeText(data).then(() => {
toast({
title: `"${pin}" data copied to clipboard!`,
duration: 2000,
});
});
};
return (
<>
{title && <strong className="mt-2 flex justify-center">{title}</strong>}
<Table className="cursor-default select-text">
<TableHeader>
<TableRow>
<TableHead>Pin</TableHead>
<TableHead>Data</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{Object.entries(data).map(([key, value]) => (
<TableRow className="group" key={key}>
<TableCell className="cursor-text">
{beautifyString(key)}
</TableCell>
<TableCell className="cursor-text">
<div className="flex min-h-9 items-center">
<Button
className="absolute right-1 top-auto m-1 hidden p-2 group-hover:block"
variant="outline"
size="icon"
onClick={() =>
copyData(
beautifyString(key),
value
.map((i) =>
typeof i === "object"
? JSON.stringify(i)
: String(i),
)
.join(", "),
)
}
title="Copy Data"
>
<Clipboard size={18} />
</Button>
{value
.map((i) => {
const text =
typeof i === "object" ? JSON.stringify(i) : String(i);
return truncateLongData && text.length > maxChars
? text.slice(0, maxChars) + "..."
: text;
})
.join(", ")}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,60 +1,107 @@
import React, { FC, useEffect, useRef } from "react";
import React, { FC, useEffect, useState } from "react";
import { Button } from "./ui/button";
import { Textarea } from "./ui/textarea";
import { Maximize2, Minimize2, Clipboard } from "lucide-react";
import { createPortal } from "react-dom";
import { toast } from "./ui/use-toast";
interface ModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (value: string) => void;
value: string;
title?: string;
defaultValue: string;
}
const InputModalComponent: FC<ModalProps> = ({
isOpen,
onClose,
onSave,
value,
title,
defaultValue,
}) => {
const [tempValue, setTempValue] = React.useState(value);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const [tempValue, setTempValue] = useState(defaultValue);
const [isMaximized, setIsMaximized] = useState(false);
useEffect(() => {
if (isOpen) {
setTempValue(value);
if (textAreaRef.current) {
textAreaRef.current.select();
}
setTempValue(defaultValue);
setIsMaximized(false);
}
}, [isOpen, value]);
}, [isOpen, defaultValue]);
const handleSave = () => {
onSave(tempValue);
onClose();
};
const toggleSize = () => {
setIsMaximized(!isMaximized);
};
const copyValue = () => {
navigator.clipboard.writeText(tempValue).then(() => {
toast({
title: "Input value copied to clipboard!",
duration: 2000,
});
});
};
if (!isOpen) {
return null;
}
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>
const modalContent = (
<div
id="modal-content"
className={`fixed rounded-lg border-[1.5px] bg-white p-5 ${
isMaximized ? "inset-[128px] flex flex-col" : `w-[90%] max-w-[800px]`
}`}
>
<h2 className="mb-4 text-center text-lg font-semibold">
{title || "Enter input text"}
</h2>
<div className="nowheel relative flex-grow">
<Textarea
ref={textAreaRef}
className="w-full h-[200px] p-2.5 rounded border border-[#dfdfdf] text-black bg-[#dfdfdf]"
className="h-full min-h-[200px] w-full resize-none"
value={tempValue}
onChange={(e) => setTempValue(e.target.value)}
/>
<div className="flex justify-end gap-2.5 mt-2.5">
<Button onClick={onClose}>Cancel</Button>
<Button onClick={handleSave}>Save</Button>
<div className="absolute bottom-2 right-2 flex space-x-2">
<Button onClick={copyValue} size="icon" variant="outline">
<Clipboard size={18} />
</Button>
<Button onClick={toggleSize} size="icon" variant="outline">
{isMaximized ? <Minimize2 size={18} /> : <Maximize2 size={18} />}
</Button>
</div>
</div>
<div className="mt-4 flex justify-end space-x-2">
<Button onClick={onClose} variant="outline">
Cancel
</Button>
<Button onClick={handleSave}>Save</Button>
</div>
</div>
);
return (
<>
{isMaximized ? (
createPortal(
<div className="fixed inset-0 flex items-center justify-center bg-white bg-opacity-60">
{modalContent}
</div>,
document.body,
)
) : (
<div className="nodrag fixed inset-0 flex items-center justify-center bg-white bg-opacity-60">
{modalContent}
</div>
)}
</>
);
};
export default InputModalComponent;

View File

@ -21,8 +21,8 @@ export async function NavBar() {
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 z-50">
<div className="flex items-center gap-4 flex-1">
<header className="sticky top-0 z-50 flex h-16 items-center gap-4 border-b bg-background px-4 md:px-6">
<div className="flex flex-1 items-center gap-4">
<Sheet>
<SheetTrigger asChild>
<Button
@ -37,20 +37,20 @@ export async function NavBar() {
<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 "
href="/"
className="flex flex-row gap-2 text-muted-foreground hover:text-foreground"
>
<IconSquareActivity /> Monitor
</Link>
<Link
href="/build"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2"
className="flex flex-row gap-2 text-muted-foreground hover:text-foreground"
>
<IconWorkFlow /> Build
</Link>
<Link
href="/marketplace"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2"
className="flex flex-row gap-2 text-muted-foreground hover:text-foreground"
>
<IconPackage2 /> Marketplace
</Link>
@ -59,26 +59,26 @@ export async function NavBar() {
</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"
href="/"
className="flex flex-row items-center gap-2 text-muted-foreground hover:text-foreground"
>
<IconSquareActivity /> Monitor
</Link>
<Link
href="/build"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 items-center"
className="flex flex-row items-center gap-2 text-muted-foreground hover:text-foreground"
>
<IconWorkFlow /> Build
</Link>
<Link
href="/marketplace"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 items-center"
className="flex flex-row items-center gap-2 text-muted-foreground hover:text-foreground"
>
<IconPackage2 /> Marketplace
</Link>
</nav>
</div>
<div className="flex-1 flex justify-center relative">
<div className="relative flex flex-1 justify-center">
<a
className="pointer-events-auto flex place-items-center gap-2"
href="https://news.agpt.co/"
@ -95,11 +95,11 @@ export async function NavBar() {
/>
</a>
</div>
<div className="flex items-center gap-4 flex-1 justify-end">
<div className="flex flex-1 items-center justify-end gap-4">
{isAvailable && !user && (
<Link
href="/login"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 items-center"
className="flex flex-row items-center gap-2 text-muted-foreground hover:text-foreground"
>
Log In
<IconCircleUser />

View File

@ -1,7 +1,7 @@
import { BlockIOSubSchema } from "@/lib/autogpt-server-api/types";
import { beautifyString, getTypeBgColor, getTypeTextColor } from "@/lib/utils";
import { FC } from "react";
import { Handle, Position } from "reactflow";
import { Handle, Position } from "@xyflow/react";
import SchemaTooltip from "./SchemaTooltip";
type HandleProps = {
@ -23,7 +23,7 @@ const NodeHandle: FC<HandleProps> = ({
string: "text",
number: "number",
boolean: "true/false",
object: "complex",
object: "object",
array: "list",
null: "null",
};
@ -31,8 +31,8 @@ const NodeHandle: FC<HandleProps> = ({
const typeClass = `text-sm ${getTypeTextColor(schema.type || "any")} ${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">
<div className="flex flex-grow flex-col">
<span className="text-m green -mb-1 text-gray-900">
{schema.title || beautifyString(keyName)}
{isRequired ? "*" : ""}
</span>
@ -42,7 +42,7 @@ const NodeHandle: FC<HandleProps> = ({
const dot = (
<div
className={`w-4 h-4 m-1 ${isConnected ? getTypeBgColor(schema.type || "any") : "bg-gray-600"} rounded-full transition-colors duration-100 group-hover:bg-gray-300`}
className={`m-1 h-4 w-4 border-2 bg-white ${isConnected ? getTypeBgColor(schema.type || "any") : "border-gray-300"} rounded-full transition-colors duration-100 group-hover:bg-gray-300`}
/>
);
@ -53,14 +53,14 @@ const NodeHandle: FC<HandleProps> = ({
type="target"
position={Position.Left}
id={keyName}
className="group -ml-[26px]"
className="background-color: white; border: 2px solid black; width: 15px; height: 15px; border-radius: 50%; bottom: -7px; left: 20%; group -ml-[26px]"
>
<div className="pointer-events-none flex items-center">
{dot}
{label}
</div>
</Handle>
<SchemaTooltip schema={schema} />
<SchemaTooltip description={schema.description} />
</div>
);
} else {

View File

@ -1,48 +1,44 @@
import React, { FC, useEffect } from "react";
import { createPortal } from "react-dom";
import React, { FC } from "react";
import { Button } from "./ui/button";
import { Textarea } from "./ui/textarea";
import { NodeExecutionResult } from "@/lib/autogpt-server-api/types";
import DataTable from "./DataTable";
import { Separator } from "@/components/ui/separator";
interface OutputModalProps {
isOpen: boolean;
onClose: () => void;
value: string;
executionResults: {
execId: string;
data: NodeExecutionResult["output_data"];
}[];
}
const OutputModalComponent: FC<OutputModalProps> = ({
isOpen,
onClose,
value,
executionResults,
}) => {
const [tempValue, setTempValue] = React.useState(value);
useEffect(() => {
if (isOpen) {
setTempValue(value);
}
}, [isOpen, value]);
if (!isOpen) {
return null;
}
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>
<Textarea
className="w-full h-[400px] p-2.5 rounded border border-[#dfdfdf] text-black bg-[#dfdfdf]"
value={tempValue}
readOnly
/>
<div className="flex justify-end gap-2.5 mt-2.5">
return (
<div className="nodrag nowheel fixed inset-0 flex items-center justify-center bg-white bg-opacity-60">
<div className="w-[500px] max-w-[90%] rounded-lg border-[1.5px] bg-white p-5">
<strong>Output Data History</strong>
<div className="my-2 max-h-[384px] flex-grow overflow-y-auto rounded-md border-[1.5px] p-2">
{executionResults.map((data, i) => (
<>
<DataTable key={i} title={data.execId} data={data.data} />
<Separator />
</>
))}
</div>
<div className="mt-2.5 flex justify-end gap-2.5">
<Button onClick={onClose}>Close</Button>
</div>
</div>
</div>,
document.body,
</div>
);
};

View File

@ -22,14 +22,14 @@ const PasswordInput = forwardRef<HTMLInputElement, InputProps>(
type="button"
variant="ghost"
size="sm"
className="absolute top-0 right-0 h-full px-3 py-2 hover:bg-transparent"
className="absolute right-0 top-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" />
<EyeIcon className="h-4 w-4" aria-hidden="true" />
) : (
<EyeOffIcon className="w-4 h-4" aria-hidden="true" />
<EyeOffIcon className="h-4 w-4" aria-hidden="true" />
)}
<span className="sr-only">
{showPassword ? "Hide password" : "Show password"}

View File

@ -14,21 +14,34 @@ import useUser from "@/hooks/useUser";
const ProfileDropdown = () => {
const { supabase } = useSupabase();
const router = useRouter();
const { user, role, isLoading } = useUser();
if (isLoading) {
return null;
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 rounded-full">
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarImage
src={user?.user_metadata["avatar_url"]}
alt="User Avatar"
/>
<AvatarFallback>CN</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => router.push("profile")}>
<DropdownMenuItem onClick={() => router.push("/profile")}>
Profile
</DropdownMenuItem>
{role === "admin" && (
<DropdownMenuItem onClick={() => router.push("/admin/dashboard")}>
Admin Dashboard
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => supabase?.auth.signOut()}>
Log out
</DropdownMenuItem>

View File

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

View File

@ -4,28 +4,31 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { BlockIOSubSchema } from "@/lib/autogpt-server-api/types";
import { Info } from "lucide-react";
import ReactMarkdown from "react-markdown";
const SchemaTooltip: React.FC<{ schema: BlockIOSubSchema }> = ({ schema }) => {
if (!schema.description) return null;
const SchemaTooltip: React.FC<{ description?: string }> = ({ description }) => {
if (!description) return null;
return (
<TooltipProvider delayDuration={400}>
<Tooltip>
<TooltipTrigger asChild>
<Info className="p-1 rounded-full hover:bg-gray-300" size={24} />
<Info className="rounded-full p-1 hover:bg-gray-300" size={24} />
</TooltipTrigger>
<TooltipContent className="max-w-xs tooltip-content">
<TooltipContent className="tooltip-content max-w-xs">
<ReactMarkdown
components={{
a: ({ node, ...props }) => (
<a className="text-blue-400 underline" {...props} />
<a
target="_blank"
className="text-blue-400 underline"
{...props}
/>
),
}}
>
{schema.description}
{description}
</ReactMarkdown>
</TooltipContent>
</Tooltip>

View File

@ -4,6 +4,7 @@ import { createClient } from "@/lib/supabase/client";
import { SupabaseClient } from "@supabase/supabase-js";
import { useRouter } from "next/navigation";
import { createContext, useContext, useEffect, useState } from "react";
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
type SupabaseContextType = {
supabase: SupabaseClient | null;
@ -25,13 +26,17 @@ export default function SupabaseProvider({
const initializeSupabase = async () => {
setIsLoading(true);
const client = createClient();
const api = new AutoGPTServerAPI();
setSupabase(client);
setIsLoading(false);
if (client) {
const {
data: { subscription },
} = client.auth.onAuthStateChange(() => {
} = client.auth.onAuthStateChange((event, session) => {
if (event === "SIGNED_IN") {
api.createUser();
}
router.refresh();
});

View File

@ -41,8 +41,16 @@ const TallyPopupSimple = () => {
return null; // Hide the button when the form is visible
}
const resetTutorial = () => {
const url = `${window.location.origin}/build?resetTutorial=true`;
window.location.href = url;
};
return (
<div className="fixed bottom-6 right-6 p-3 transition-all duration-300 ease-in-out z-50">
<div className="fixed bottom-6 right-6 z-50 flex items-center gap-4 p-3 transition-all duration-300 ease-in-out">
<Button variant="default" onClick={resetTutorial} className="mb-0">
Tutorial
</Button>
<Button
variant="default"
data-tally-open="3yx2L0"

View File

@ -0,0 +1,149 @@
"use client";
import {
Dialog,
DialogContent,
DialogClose,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorInput,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from "@/components/ui/multiselect";
import { Controller, useForm } from "react-hook-form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useState } from "react";
import { addFeaturedAgent } from "./actions";
import { Agent } from "@/lib/marketplace-api/types";
type FormData = {
agent: string;
categories: string[];
};
export const AdminAddFeaturedAgentDialog = ({
categories,
agents,
}: {
categories: string[];
agents: Agent[];
}) => {
const [selectedAgent, setSelectedAgent] = useState<string>("");
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const {
control,
handleSubmit,
watch,
setValue,
formState: { errors },
} = useForm<FormData>({
defaultValues: {
agent: "",
categories: [],
},
});
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="sm">
Add Featured Agent
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add Featured Agent</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4">
<Controller
name="agent"
control={control}
rules={{ required: true }}
render={({ field }) => (
<div>
<label htmlFor={field.name}>Agent</label>
<Select
onValueChange={(value) => {
field.onChange(value);
setSelectedAgent(value);
}}
value={field.value || ""}
>
<SelectTrigger>
<SelectValue placeholder="Select an agent" />
</SelectTrigger>
<SelectContent>
{/* Populate with agents */}
{agents.map((agent) => (
<SelectItem key={agent.id} value={agent.id}>
{agent.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
/>
<Controller
name="categories"
control={control}
render={({ field }) => (
<MultiSelector
values={field.value || []}
onValuesChange={(values) => {
field.onChange(values);
setSelectedCategories(values);
}}
>
<MultiSelectorTrigger>
<MultiSelectorInput placeholder="Select categories" />
</MultiSelectorTrigger>
<MultiSelectorContent>
<MultiSelectorList>
{categories.map((category) => (
<MultiSelectorItem key={category} value={category}>
{category}
</MultiSelectorItem>
))}
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
)}
/>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button
type="submit"
onClick={async () => {
// Handle adding the featured agent
await addFeaturedAgent(selectedAgent, selectedCategories);
// close the dialog
}}
>
Add
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@ -0,0 +1,67 @@
import { Button } from "@/components/ui/button";
import {
getFeaturedAgents,
removeFeaturedAgent,
getCategories,
getNotFeaturedAgents,
} from "./actions";
import FeaturedAgentsTable from "./FeaturedAgentsTable";
import { AdminAddFeaturedAgentDialog } from "./AdminAddFeaturedAgentDialog";
import { revalidatePath } from "next/cache";
export default async function AdminFeaturedAgentsControl({
className,
}: {
className?: string;
}) {
// add featured agent button
// modal to select agent?
// modal to select categories?
// table of featured agents
// in table
// remove featured agent button
// edit featured agent categories button
// table footer
// Next page button
// Previous page button
// Page number input
// Page size input
// Total pages input
// Go to page button
const page = 1;
const pageSize = 10;
const agents = await getFeaturedAgents(page, pageSize);
const categories = await getCategories();
const notFeaturedAgents = await getNotFeaturedAgents();
return (
<div className={`flex flex-col gap-4 ${className}`}>
<div className="mb-4 flex justify-between">
<h3 className="text-lg font-semibold">Featured Agent Controls</h3>
<AdminAddFeaturedAgentDialog
categories={categories.unique_categories}
agents={notFeaturedAgents.agents}
/>
</div>
<FeaturedAgentsTable
agents={agents.agents}
globalActions={[
{
component: <Button>Remove</Button>,
action: async (rows) => {
"use server";
const all = rows.map((row) => removeFeaturedAgent(row.id));
await Promise.all(all);
revalidatePath("/marketplace");
},
},
]}
/>
</div>
);
}

View File

@ -0,0 +1,36 @@
import { Agent } from "@/lib/marketplace-api";
import AdminMarketplaceCard from "./AdminMarketplaceCard";
import { ClipboardX } from "lucide-react";
export default function AdminMarketplaceAgentList({
agents,
className,
}: {
agents: Agent[];
className?: string;
}) {
if (agents.length === 0) {
return (
<div className={className}>
<h3 className="text-lg font-semibold">Agents to review</h3>
<div className="flex flex-col items-center justify-center py-12 text-gray-500">
<ClipboardX size={48} />
<p className="mt-4 text-lg font-semibold">No agents to review</p>
</div>
</div>
);
}
return (
<div className={`flex flex-col gap-4 ${className}`}>
<div>
<h3 className="text-lg font-semibold">Agents to review</h3>
</div>
<div className="flex flex-col gap-4">
{agents.map((agent) => (
<AdminMarketplaceCard agent={agent} key={agent.id} />
))}
</div>
</div>
);
}

View File

@ -0,0 +1,113 @@
"use client";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { approveAgent, rejectAgent } from "./actions";
import { Agent } from "@/lib/marketplace-api";
import Link from "next/link";
import { useState } from "react";
import { Input } from "@/components/ui/input";
function AdminMarketplaceCard({ agent }: { agent: Agent }) {
const [isApproved, setIsApproved] = useState(false);
const [isRejected, setIsRejected] = useState(false);
const [comment, setComment] = useState("");
const approveAgentWithId = approveAgent.bind(
null,
agent.id,
agent.version,
comment,
);
const rejectAgentWithId = rejectAgent.bind(
null,
agent.id,
agent.version,
comment,
);
const handleApprove = async (e: React.FormEvent) => {
e.preventDefault();
await approveAgentWithId();
setIsApproved(true);
};
const handleReject = async (e: React.FormEvent) => {
e.preventDefault();
await rejectAgentWithId();
setIsRejected(true);
};
return (
<>
{!isApproved && !isRejected && (
<Card key={agent.id} className="m-3 flex h-[300px] flex-col p-4">
<div className="mb-2 flex items-start justify-between">
<Link
href={`/marketplace/${agent.id}`}
className="text-lg font-semibold hover:underline"
>
{agent.name}
</Link>
<Badge variant="outline">v{agent.version}</Badge>
</div>
<p className="mb-2 text-sm text-gray-500">by {agent.author}</p>
<ScrollArea className="flex-grow">
<p className="mb-2 text-sm text-gray-600">{agent.description}</p>
<div className="mb-2 flex flex-wrap gap-1">
{agent.categories.map((category) => (
<Badge key={category} variant="secondary">
{category}
</Badge>
))}
</div>
<div className="flex flex-wrap gap-1">
{agent.keywords.map((keyword) => (
<Badge key={keyword} variant="outline">
{keyword}
</Badge>
))}
</div>
</ScrollArea>
<div className="mb-2 flex justify-between text-xs text-gray-500">
<span>
Created: {new Date(agent.createdAt).toLocaleDateString()}
</span>
<span>
Updated: {new Date(agent.updatedAt).toLocaleDateString()}
</span>
</div>
<div className="mb-4 flex justify-between text-sm">
<span>👁 {agent.views}</span>
<span> {agent.downloads}</span>
</div>
<div className="mt-auto space-y-2">
<div className="flex justify-end space-x-2">
<Input
type="text"
placeholder="Add a comment (optional)"
value={comment}
onChange={(e) => setComment(e.target.value)}
/>
{!isRejected && (
<form onSubmit={handleReject}>
<Button variant="outline" type="submit">
Reject
</Button>
</form>
)}
{!isApproved && (
<form onSubmit={handleApprove}>
<Button type="submit">Approve</Button>
</form>
)}
</div>
</div>
</Card>
)}
</>
);
}
export default AdminMarketplaceCard;

View File

@ -0,0 +1,114 @@
"use client";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { DataTable } from "@/components/ui/data-table";
import { Agent } from "@/lib/marketplace-api";
import { ColumnDef } from "@tanstack/react-table";
import { ArrowUpDown } from "lucide-react";
import { removeFeaturedAgent } from "./actions";
import { GlobalActions } from "@/components/ui/data-table";
export const columns: ColumnDef<Agent>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
},
{
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Name
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
accessorKey: "name",
},
{
header: "Description",
accessorKey: "description",
},
{
header: "Categories",
accessorKey: "categories",
},
{
header: "Keywords",
accessorKey: "keywords",
},
{
header: "Downloads",
accessorKey: "downloads",
},
{
header: "Author",
accessorKey: "author",
},
{
header: "Version",
accessorKey: "version",
},
{
header: "actions",
cell: ({ row }) => {
const handleRemove = async () => {
await removeFeaturedAgentWithId();
};
// const handleEdit = async () => {
// console.log("edit");
// };
const removeFeaturedAgentWithId = removeFeaturedAgent.bind(
null,
row.original.id,
);
return (
<div className="flex justify-end gap-2">
<Button variant="outline" size="sm" onClick={handleRemove}>
Remove
</Button>
{/* <Button variant="outline" size="sm" onClick={handleEdit}>
Edit
</Button> */}
</div>
);
},
},
];
export default function FeaturedAgentsTable({
agents,
globalActions,
}: {
agents: Agent[];
globalActions: GlobalActions<Agent>[];
}) {
return (
<DataTable
columns={columns}
data={agents}
filterPlaceholder="Search agents..."
filterColumn="name"
globalActions={globalActions}
/>
);
}

View File

@ -0,0 +1,85 @@
"use server";
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
import MarketplaceAPI from "@/lib/marketplace-api";
import { revalidatePath } from "next/cache";
export async function approveAgent(
agentId: string,
version: number,
comment: string,
) {
const api = new MarketplaceAPI();
await api.approveAgentSubmission(agentId, version, comment);
console.debug(`Approving agent ${agentId}`);
revalidatePath("/marketplace");
}
export async function rejectAgent(
agentId: string,
version: number,
comment: string,
) {
const api = new MarketplaceAPI();
await api.rejectAgentSubmission(agentId, version, comment);
console.debug(`Rejecting agent ${agentId}`);
revalidatePath("/marketplace");
}
export async function getReviewableAgents() {
const api = new MarketplaceAPI();
return api.getAgentSubmissions();
}
export async function getFeaturedAgents(
page: number = 1,
pageSize: number = 10,
) {
const api = new MarketplaceAPI();
const featured = await api.getFeaturedAgents(page, pageSize);
console.debug(`Getting featured agents ${featured.agents.length}`);
return featured;
}
export async function getFeaturedAgent(agentId: string) {
const api = new MarketplaceAPI();
const featured = await api.getFeaturedAgent(agentId);
console.debug(`Getting featured agent ${featured.agentId}`);
return featured;
}
export async function addFeaturedAgent(
agentId: string,
categories: string[] = ["featured"],
) {
const api = new MarketplaceAPI();
await api.addFeaturedAgent(agentId, categories);
console.debug(`Adding featured agent ${agentId}`);
revalidatePath("/marketplace");
}
export async function removeFeaturedAgent(
agentId: string,
categories: string[] = ["featured"],
) {
const api = new MarketplaceAPI();
await api.removeFeaturedAgent(agentId, categories);
console.debug(`Removing featured agent ${agentId}`);
revalidatePath("/marketplace");
}
export async function getCategories() {
const api = new MarketplaceAPI();
const categories = await api.getCategories();
console.debug(`Getting categories ${categories.unique_categories.length}`);
return categories;
}
export async function getNotFeaturedAgents(
page: number = 1,
pageSize: number = 100,
) {
const api = new MarketplaceAPI();
const agents = await api.getNotFeaturedAgents(page, pageSize);
console.debug(`Getting not featured agents ${agents.agents.length}`);
return agents;
}

View File

@ -21,8 +21,13 @@ import AutoGPTServerAPI, {
import { cn } from "@/lib/utils";
import { EnterIcon } from "@radix-ui/react-icons";
// Add this custom schema for File type
const fileSchema = z.custom<File>((val) => val instanceof File, {
message: "Must be a File object",
});
const formSchema = z.object({
agentFile: z.instanceof(File),
agentFile: fileSchema,
agentName: z.string().min(1, "Agent name is required"),
agentDescription: z.string(),
importAsTemplate: z.boolean(),
@ -165,7 +170,7 @@ export const AgentImportForm: React.FC<
<FormItem>
<FormLabel>Import as</FormLabel>
<FormControl>
<div className="flex space-x-2 items-center">
<div className="flex items-center space-x-2">
<span
className={
field.value ? "text-gray-400 dark:text-gray-600" : ""

View File

@ -38,3 +38,7 @@
.react-flow__edge-interaction {
cursor: pointer;
}
.react-flow__edges > svg:has(> g.selected) {
z-index: 10 !important;
}

View File

@ -1,10 +1,5 @@
.custom-node {
@apply p-3;
border: 3px solid #4b5563;
border-radius: 12px;
background: #ffffff;
color: #000000;
width: 500px;
box-sizing: border-box;
transition: border-color 0.3s ease-in-out;
}
@ -30,7 +25,6 @@
margin-bottom: 0px;
padding: 5px;
min-height: 44px;
width: 100%;
height: 100%;
}
@ -105,16 +99,6 @@
margin-top: 5px;
}
.node-output {
margin-top: 5px;
margin-bottom: 5px;
background: #fff;
border: 1px solid #000; /* Border for output section */
padding: 10px;
border-radius: 10px;
width: 100%;
}
.error-message {
color: #d9534f;
font-size: 13px;
@ -140,7 +124,7 @@
}
.queued {
border-color: #25e6e6; /* Cyanic border for failed nodes */
border-color: #25e6e6; /* Cyan border for queued nodes */
}
.custom-switch {

View File

@ -14,10 +14,19 @@ import {
import { Block } from "@/lib/autogpt-server-api";
import { PlusIcon } from "@radix-ui/react-icons";
import { IconToyBrick } from "@/components/ui/icons";
import SchemaTooltip from "@/components/SchemaTooltip";
import { getPrimaryCategoryColor } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
interface BlocksControlProps {
blocks: Block[];
addBlock: (id: string, name: string) => void;
pinBlocksPopover: boolean;
}
/**
@ -32,30 +41,61 @@ interface BlocksControlProps {
export const BlocksControl: React.FC<BlocksControlProps> = ({
blocks,
addBlock,
pinBlocksPopover,
}) => {
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const filteredBlocks = blocks.filter((block: Block) =>
block.name.toLowerCase().includes(searchQuery.toLowerCase()),
// Extract unique categories from blocks
const categories = Array.from(
new Set(
blocks.flatMap((block) => block.categories.map((cat) => cat.category)),
),
);
const filteredBlocks = blocks.filter(
(block: Block) =>
(block.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
beautifyString(block.name)
.toLowerCase()
.includes(searchQuery.toLowerCase())) &&
(!selectedCategory ||
block.categories.some((cat) => cat.category === selectedCategory)),
);
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon">
<IconToyBrick />
</Button>
</PopoverTrigger>
<Popover open={pinBlocksPopover ? true : undefined}>
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon"
data-id="blocks-control-popover-trigger"
>
<IconToyBrick />
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent side="right">Blocks</TooltipContent>
</Tooltip>
<PopoverContent
side="right"
sideOffset={22}
align="start"
className="w-80 p-0"
className="w-[30rem] p-0"
data-id="blocks-control-popover-content"
>
<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>
<Card className="border-none shadow-md">
<CardHeader className="flex flex-col gap-x-8 gap-y-2 p-3 px-2">
<div className="items-center justify-between">
<Label
htmlFor="search-blocks"
className="whitespace-nowrap border-b-2 border-violet-500 text-base font-semibold text-black 2xl:text-xl"
data-id="blocks-control-label"
>
Blocks
</Label>
</div>
<Input
id="search-blocks"
@ -63,24 +103,58 @@ export const BlocksControl: React.FC<BlocksControlProps> = ({
placeholder="Search blocks..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
data-id="blocks-control-search-input"
/>
<div className="mt-2 flex flex-wrap gap-2">
{categories.map((category) => (
<Badge
key={category}
variant={
selectedCategory === category ? "default" : "outline"
}
className={`cursor-pointer ${getPrimaryCategoryColor([{ category, description: "" }])}`}
onClick={() =>
setSelectedCategory(
selectedCategory === category ? null : category,
)
}
>
{beautifyString(category)}
</Badge>
))}
</div>
</CardHeader>
<CardContent className="p-1">
<ScrollArea className="h-[60vh]">
<ScrollArea
className="h-[60vh]"
data-id="blocks-control-scroll-area"
>
{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">
<Card
key={block.id}
className={`m-2 ${getPrimaryCategoryColor(block.categories)}`}
data-id={`block-card-${block.id}`}
>
<div className="m-3 flex items-center justify-between">
<div className="mr-2 min-w-0 flex-1">
<span
className="block truncate font-medium"
data-id={`block-name-${block.id}`}
>
{beautifyString(block.name)}
</span>
</div>
<div className="flex items-center gap-1 flex-shrink-0">
<SchemaTooltip description={block.description} />
<div
className="flex flex-shrink-0 items-center gap-1"
data-id={`block-tooltip-${block.id}`}
>
<Button
variant="ghost"
size="icon"
onClick={() => addBlock(block.id, block.name)}
aria-label="Add block"
data-id={`add-block-button-${block.id}`}
>
<PlusIcon />
</Button>

View File

@ -44,7 +44,7 @@ export const ControlPanel = ({
return (
<Card className={cn("w-14", className)}>
<CardContent className="p-0">
<div className="flex flex-col items-center gap-8 px-2 sm:py-5 rounded-radius">
<div className="rounded-radius flex flex-col items-center gap-8 px-2 sm:py-5">
{children}
<Separator />
{controls.map((control, index) => (
@ -54,6 +54,7 @@ export const ControlPanel = ({
variant="ghost"
size="icon"
onClick={() => control.onClick()}
data-id={`control-button-${index}`}
>
{control.icon}
<span className="sr-only">{control.label}</span>

View File

@ -10,6 +10,11 @@ import { Button } from "@/components/ui/button";
import { GraphMeta } from "@/lib/autogpt-server-api";
import { Label } from "@/components/ui/label";
import { IconSave } from "@/components/ui/icons";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
interface SaveControlProps {
agentMeta: GraphMeta | null;
@ -51,11 +56,16 @@ export const SaveControl = ({
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon">
<IconSave />
</Button>
</PopoverTrigger>
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon">
<IconSave />
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent side="right">Save</TooltipContent>
</Tooltip>
<PopoverContent side="right" sideOffset={15} align="start">
<Card className="border-none shadow-none">
<CardContent className="p-4">
@ -78,7 +88,7 @@ export const SaveControl = ({
/>
</div>
</CardContent>
<CardFooter className="flex flex-col items-stretch gap-2 ">
<CardFooter className="flex flex-col items-stretch gap-2">
<Button className="w-full" onClick={handleSave}>
Save {getType()}
</Button>

View File

@ -1,7 +1,7 @@
// history.ts
import { CustomNodeData } from "./CustomNode";
import { CustomEdgeData } from "./CustomEdge";
import { Edge } from "reactflow";
import { Edge } from "@xyflow/react";
type ActionType =
| "ADD_NODE"

View File

@ -1,6 +1,6 @@
"use client";
import { useMemo, useState } from "react";
import { useState } from "react";
import Link from "next/link";
import {
ArrowLeft,
@ -11,30 +11,31 @@ import {
ChevronUp,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { AgentDetailResponse } from "@/lib/marketplace-api";
import {
AgentDetailResponse,
InstallationLocation,
} from "@/lib/marketplace-api";
import dynamic from "next/dynamic";
import { Node, Edge, NodeTypes, EdgeTypes } from "reactflow";
import { Node, Edge } from "@xyflow/react";
import MarketplaceAPI from "@/lib/marketplace-api";
import AutoGPTServerAPI, { GraphCreatable } from "@/lib/autogpt-server-api";
const ReactFlow = dynamic(
() => import("reactflow").then((mod) => mod.default),
() => import("@xyflow/react").then((mod) => mod.ReactFlow),
{ ssr: false },
);
const Controls = dynamic(
() => import("reactflow").then((mod) => mod.Controls),
() => import("@xyflow/react").then((mod) => mod.Controls),
{ ssr: false },
);
const Background = dynamic(
() => import("reactflow").then((mod) => mod.Background),
() => import("@xyflow/react").then((mod) => mod.Background),
{ ssr: false },
);
import "reactflow/dist/style.css";
import CustomNode from "./CustomNode";
import { CustomEdge } from "./CustomEdge";
import ConnectionLine from "./ConnectionLine";
import "@xyflow/react/dist/style.css";
import { beautifyString } from "@/lib/utils";
import { makeAnalyticsEvent } from "./actions";
function convertGraphToReactFlow(graph: any): { nodes: Node[]; edges: Edge[] } {
const nodes: Node[] = graph.nodes.map((node: any) => {
@ -99,8 +100,16 @@ async function installGraph(id: string): Promise<void> {
nodes: agent.graph.nodes,
links: agent.graph.links,
};
await serverAPI.createTemplate(data);
console.log(`Agent installed successfully`);
const result = await serverAPI.createTemplate(data);
makeAnalyticsEvent({
event_name: "agent_installed_from_marketplace",
event_data: {
marketplace_agent_id: id,
installed_agent_id: result.id,
installation_location: InstallationLocation.CLOUD,
},
});
console.log(`Agent installed successfully`, result);
} catch (error) {
console.error(`Error installing agent:`, error);
throw error;
@ -111,12 +120,9 @@ function AgentDetailContent({ agent }: { agent: AgentDetailResponse }) {
const [isGraphExpanded, setIsGraphExpanded] = useState(false);
const { nodes, edges } = convertGraphToReactFlow(agent.graph);
const nodeTypes: NodeTypes = useMemo(() => ({ custom: CustomNode }), []);
const edgeTypes: EdgeTypes = useMemo(() => ({ custom: CustomEdge }), []);
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex justify-between items-center mb-4">
<div className="mx-auto max-w-7xl px-4 py-4 sm:px-6 lg:px-8">
<div className="mb-4 flex items-center justify-between">
<Link
href="/marketplace"
className="inline-flex items-center text-indigo-600 hover:text-indigo-500"
@ -126,13 +132,13 @@ function AgentDetailContent({ agent }: { agent: AgentDetailResponse }) {
</Link>
<Button
onClick={() => installGraph(agent.id)}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
className="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
<Download className="mr-2" size={16} />
Download Agent
</Button>
</div>
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
<div className="overflow-hidden bg-white shadow sm:rounded-lg">
<div className="px-4 py-5 sm:px-6">
<h1 className="text-3xl font-bold text-gray-900">{agent.name}</h1>
<p className="mt-1 max-w-2xl text-sm text-gray-500">
@ -141,60 +147,26 @@ function AgentDetailContent({ agent }: { agent: AgentDetailResponse }) {
</div>
<div className="border-t border-gray-200 px-4 py-5 sm:p-0">
<dl className="sm:divide-y sm:divide-gray-200">
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-gray-500 flex items-center">
<div className="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<dt className="flex items-center text-sm font-medium text-gray-500">
<Calendar className="mr-2" size={16} />
Last Updated
</dt>
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
<dd className="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{new Date(agent.updatedAt).toLocaleDateString()}
</dd>
</div>
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt className="text-sm font-medium text-gray-500 flex items-center">
<div className="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<dt className="flex items-center text-sm font-medium text-gray-500">
<Tag className="mr-2" size={16} />
Categories
</dt>
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
<dd className="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{agent.categories.join(", ")}
</dd>
</div>
</dl>
</div>
<div className="border-t border-gray-200 px-4 py-5 sm:px-6">
<button
className="flex items-center justify-between w-full text-left text-sm font-medium text-indigo-600 hover:text-indigo-500"
onClick={() => setIsGraphExpanded(!isGraphExpanded)}
>
<span>Agent Graph</span>
{isGraphExpanded ? (
<ChevronUp size={20} />
) : (
<ChevronDown size={20} />
)}
</button>
{isGraphExpanded && (
<div className="mt-4" style={{ height: "600px" }}>
<ReactFlow
nodes={nodes}
edges={edges}
// nodeTypes={nodeTypes}
// edgeTypes={edgeTypes}
// connectionLineComponent={ConnectionLine}
fitView
attributionPosition="bottom-left"
nodesConnectable={false}
nodesDraggable={false}
zoomOnScroll={false}
panOnScroll={false}
elementsSelectable={false}
>
<Controls showInteractive={false} />
<Background />
</ReactFlow>
</div>
)}
</div>
</div>
</div>
);

View File

@ -0,0 +1,9 @@
"use server";
import MarketplaceAPI, { AnalyticsEvent } from "@/lib/marketplace-api";
export async function makeAnalyticsEvent(event: AnalyticsEvent) {
const apiUrl = process.env.AGPT_SERVER_API_URL;
const api = new MarketplaceAPI();
await api.makeAnalyticsEvent(event);
}

View File

@ -1,5 +1,5 @@
import AutoGPTServerAPI, { GraphMeta } from "@/lib/autogpt-server-api";
import React, { useEffect, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import Link from "next/link";
@ -29,6 +29,7 @@ import {
} from "@/components/ui/table";
import moment from "moment/moment";
import { FlowRun } from "@/lib/types";
import { DialogTitle } from "@/components/ui/dialog";
export const AgentFlowList = ({
flows,
@ -44,14 +45,14 @@ export const AgentFlowList = ({
className?: string;
}) => {
const [templates, setTemplates] = useState<GraphMeta[]>([]);
const api = new AutoGPTServerAPI();
const api = useMemo(() => new AutoGPTServerAPI(), []);
useEffect(() => {
api.listTemplates().then((templates) => setTemplates(templates));
}, []);
}, [api]);
return (
<Card className={className}>
<CardHeader className="flex-row justify-between items-center space-x-3 space-y-0">
<CardHeader className="flex-row items-center justify-between space-x-3 space-y-0">
<CardTitle>Agents</CardTitle>
<div className="flex items-center">
@ -102,8 +103,11 @@ export const AgentFlowList = ({
</DropdownMenu>
<DialogContent>
<DialogHeader className="text-lg">
Import an Agent (template) from a file
<DialogHeader>
<DialogTitle className="sr-only">Import Agent</DialogTitle>
<h2 className="text-lg font-semibold">
Import an Agent (template) from a file
</h2>
</DialogHeader>
<AgentImportForm />
</DialogContent>

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import AutoGPTServerAPI, {
Graph,
GraphMeta,
@ -28,7 +28,7 @@ export const FlowInfo: React.FC<
flowVersion?: number | "all";
}
> = ({ flow, flowRuns, flowVersion, ...props }) => {
const api = new AutoGPTServerAPI();
const api = useMemo(() => new AutoGPTServerAPI(), []);
const [flowVersions, setFlowVersions] = useState<Graph[] | null>(null);
const [selectedVersion, setSelectedFlowVersion] = useState(
@ -41,11 +41,11 @@ export const FlowInfo: React.FC<
useEffect(() => {
api.getGraphAllVersions(flow.id).then((result) => setFlowVersions(result));
}, [flow.id]);
}, [flow.id, api]);
return (
<Card {...props}>
<CardHeader className="flex-row justify-between space-y-0 space-x-3">
<CardHeader className="flex-row justify-between space-x-3 space-y-0">
<div>
<CardTitle>
{flow.name} <span className="font-light">v{flow.version}</span>

View File

@ -1,9 +1,10 @@
import React from "react";
import { GraphMeta } from "@/lib/autogpt-server-api";
import React, { useCallback } from "react";
import AutoGPTServerAPI, { GraphMeta } from "@/lib/autogpt-server-api";
import { FlowRun } from "@/lib/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import Link from "next/link";
import { buttonVariants } from "@/components/ui/button";
import { Button, buttonVariants } from "@/components/ui/button";
import { IconSquare } from "@/components/ui/icons";
import { Pencil2Icon } from "@radix-ui/react-icons";
import moment from "moment/moment";
import { FlowRunStatusBadge } from "@/components/monitor/FlowRunStatusBadge";
@ -20,9 +21,14 @@ export const FlowRunInfo: React.FC<
);
}
const handleStopRun = useCallback(() => {
const api = new AutoGPTServerAPI();
api.stopGraphExecution(flow.id, flowRun.id);
}, [flow.id, flowRun.id]);
return (
<Card {...props}>
<CardHeader className="flex-row items-center justify-between space-y-0 space-x-3">
<CardHeader className="flex-row items-center justify-between space-x-3 space-y-0">
<div>
<CardTitle>
{flow.name} <span className="font-light">v{flow.version}</span>
@ -34,12 +40,19 @@ export const FlowRunInfo: React.FC<
Run ID: <code>{flowRun.id}</code>
</p>
</div>
<Link
className={buttonVariants({ variant: "outline" })}
href={`/build?flowID=${flow.id}`}
>
<Pencil2Icon className="mr-2" /> Edit Agent
</Link>
<div className="flex space-x-2">
{flowRun.status === "running" && (
<Button onClick={handleStopRun} variant="destructive">
<IconSquare className="mr-2" /> Stop Run
</Button>
)}
<Link
className={buttonVariants({ variant: "outline" })}
href={`/build?flowID=${flow.id}`}
>
<Pencil2Icon className="mr-2" /> Edit Agent
</Link>
</div>
</CardHeader>
<CardContent>
<p>

View File

@ -143,7 +143,7 @@ const ScrollableLegend: React.FC<
return (
<div
className={cn(
"whitespace-nowrap px-4 text-sm overflow-x-auto space-x-3",
"space-x-3 overflow-x-auto whitespace-nowrap px-4 text-sm",
className,
)}
style={{ scrollbarWidth: "none" }}
@ -153,7 +153,7 @@ const ScrollableLegend: React.FC<
return (
<span key={`item-${index}`} className="inline-flex items-center">
<span
className="size-2.5 inline-block mr-1 rounded-full"
className="mr-1 inline-block size-2.5 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<span>{entry.value}</span>

View File

@ -0,0 +1,24 @@
export default function AgentsFlowListSkeleton() {
return (
<div className="mx-auto max-w-4xl p-4">
<div className="mb-4 flex items-center justify-between">
<h1 className="text-2xl font-bold">Agents</h1>
<div className="h-10 w-24 animate-pulse rounded bg-gray-200"></div>
</div>
<div className="rounded-lg bg-white p-4 shadow">
<div className="mb-4 grid grid-cols-3 gap-4 font-medium text-gray-500">
<div>Name</div>
<div># of runs</div>
<div>Last run</div>
</div>
{[...Array(3)].map((_, index) => (
<div key={index} className="mb-4 grid grid-cols-3 gap-4">
<div className="h-6 animate-pulse rounded bg-gray-200"></div>
<div className="h-6 animate-pulse rounded bg-gray-200"></div>
<div className="h-6 animate-pulse rounded bg-gray-200"></div>
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,23 @@
export default function FlowRunsListSkeleton() {
return (
<div className="mx-auto max-w-4xl p-4">
<div className="rounded-lg bg-white p-4 shadow">
<h2 className="mb-4 text-xl font-semibold">Runs</h2>
<div className="mb-4 grid grid-cols-4 gap-4 text-sm font-medium text-gray-500">
<div>Agent</div>
<div>Started</div>
<div>Status</div>
<div>Duration</div>
</div>
{[...Array(4)].map((_, index) => (
<div key={index} className="mb-4 grid grid-cols-4 gap-4">
<div className="h-5 animate-pulse rounded bg-gray-200"></div>
<div className="h-5 animate-pulse rounded bg-gray-200"></div>
<div className="h-5 animate-pulse rounded bg-gray-200"></div>
<div className="h-5 animate-pulse rounded bg-gray-200"></div>
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,28 @@
export default function FlowRunsStatusSkeleton() {
return (
<div className="mx-auto max-w-4xl p-4">
<div className="rounded-lg bg-white p-4 shadow">
<div className="mb-6 flex items-center justify-between">
<h2 className="text-xl font-semibold">Stats</h2>
<div className="flex space-x-2">
{["2h", "8h", "24h", "7d", "Custom", "All"].map((btn) => (
<div
key={btn}
className="h-8 w-16 animate-pulse rounded bg-gray-200"
></div>
))}
</div>
</div>
{/* Placeholder for the line chart */}
<div className="mb-6 h-64 w-full animate-pulse rounded bg-gray-200"></div>
{/* Placeholders for total runs and total run time */}
<div className="space-y-2">
<div className="h-6 w-1/3 animate-pulse rounded bg-gray-200"></div>
<div className="h-6 w-1/2 animate-pulse rounded bg-gray-200"></div>
</div>
</div>
</div>
);
}

View File

@ -10,7 +10,7 @@ import {
BlockIONumberSubSchema,
BlockIOBooleanSubSchema,
} from "@/lib/autogpt-server-api/types";
import { FC, useState } from "react";
import React, { FC, useCallback, useEffect, useState } from "react";
import { Button } from "./ui/button";
import { Switch } from "./ui/switch";
import {
@ -21,11 +21,14 @@ import {
SelectValue,
} from "./ui/select";
import { Input } from "./ui/input";
import NodeHandle from "./NodeHandle";
import { ConnectionData } from "./CustomNode";
type NodeObjectInputTreeProps = {
selfKey?: string;
schema: BlockIORootSchema | BlockIOObjectSubSchema;
object?: { [key: string]: any };
connections: ConnectionData;
handleInputClick: (key: string) => void;
handleInputChange: (key: string, value: any) => void;
errors: { [key: string]: string | undefined };
@ -37,6 +40,7 @@ const NodeObjectInputTree: FC<NodeObjectInputTreeProps> = ({
selfKey = "",
schema,
object,
connections,
handleInputClick,
handleInputChange,
errors,
@ -45,7 +49,7 @@ const NodeObjectInputTree: FC<NodeObjectInputTreeProps> = ({
}) => {
object ??= ("default" in schema ? schema.default : null) ?? {};
return (
<div className={cn(className, "flex-col w-full")}>
<div className={cn(className, "w-full flex-col")}>
{displayName && <strong>{displayName}</strong>}
{Object.entries(schema.properties).map(([propKey, propSchema]) => {
const childKey = selfKey ? `${selfKey}.${propKey}` : propKey;
@ -53,7 +57,7 @@ const NodeObjectInputTree: FC<NodeObjectInputTreeProps> = ({
return (
<div
key={propKey}
className="flex flex-row justify-between space-y-2 w-full"
className="flex w-full flex-row justify-between space-y-2"
>
<span className="mr-2 mt-3">
{propSchema.title || beautifyString(propKey)}
@ -64,6 +68,7 @@ const NodeObjectInputTree: FC<NodeObjectInputTreeProps> = ({
propSchema={propSchema}
currentValue={object ? object[propKey] : undefined}
errors={errors}
connections={connections}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
displayName={propSchema.title || beautifyString(propKey)}
@ -82,6 +87,7 @@ export const NodeGenericInputField: FC<{
propSchema: BlockIOSubSchema;
currentValue?: any;
errors: NodeObjectInputTreeProps["errors"];
connections: NodeObjectInputTreeProps["connections"];
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
handleInputClick: NodeObjectInputTreeProps["handleInputClick"];
className?: string;
@ -91,6 +97,7 @@ export const NodeGenericInputField: FC<{
propSchema,
currentValue,
errors,
connections,
handleInputChange,
handleInputClick,
className,
@ -116,6 +123,7 @@ export const NodeGenericInputField: FC<{
errors={errors}
className={cn("border-l border-gray-500 pl-2", className)} // visual indent
displayName={displayName}
connections={connections}
handleInputClick={handleInputClick}
handleInputChange={handleInputChange}
/>
@ -131,6 +139,7 @@ export const NodeGenericInputField: FC<{
errors={errors}
className={className}
displayName={displayName}
connections={connections}
handleInputChange={handleInputChange}
/>
);
@ -230,10 +239,24 @@ export const NodeGenericInputField: FC<{
errors={errors}
className={className}
displayName={displayName}
connections={connections}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
/>
);
case "object":
return (
<NodeKeyValueInput
selfKey={propKey}
schema={propSchema}
entries={currentValue}
errors={errors}
className={className}
displayName={displayName}
connections={connections}
handleInputChange={handleInputChange}
/>
);
default:
console.warn(
`Schema for '${propKey}' specifies unknown type:`,
@ -259,6 +282,7 @@ const NodeKeyValueInput: FC<{
schema: BlockIOKVSubSchema;
entries?: { [key: string]: string } | { [key: string]: number };
errors: { [key: string]: string | undefined };
connections: NodeObjectInputTreeProps["connections"];
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
className?: string;
displayName?: string;
@ -266,21 +290,36 @@ const NodeKeyValueInput: FC<{
selfKey,
entries,
schema,
connections,
handleInputChange,
errors,
className,
displayName,
}) => {
const getPairValues = useCallback(() => {
let defaultEntries = new Map<string, any>();
connections
.filter((c) => c.targetHandle.startsWith(`${selfKey}_`))
.forEach((c) => {
const key = c.targetHandle.slice(`${selfKey}_#_`.length);
defaultEntries.set(key, "");
});
Object.entries(entries ?? schema.default ?? {}).forEach(([key, value]) => {
defaultEntries.set(key, value);
});
return Array.from(defaultEntries, ([key, value]) => ({ key, value }));
}, [connections, entries, schema.default, selfKey]);
const [keyValuePairs, setKeyValuePairs] = useState<
{
key: string;
value: string | number | null;
}[]
>(
Object.entries(entries ?? schema.default ?? {}).map(([key, value]) => ({
key,
value: value,
})),
{ key: string; value: string | number | null }[]
>([]);
useEffect(
() => setKeyValuePairs(getPairValues()),
[connections, entries, schema.default, getPairValues],
);
function updateKeyValuePairs(newPairs: typeof keyValuePairs) {
@ -292,54 +331,76 @@ const NodeKeyValueInput: FC<{
}
function convertValueType(value: string): string | number | null {
if (schema.additionalProperties.type == "string") return value;
if (
!schema.additionalProperties ||
schema.additionalProperties.type == "string"
)
return value;
if (!value) return null;
return Number(value);
}
function getEntryKey(key: string): string {
return `${selfKey}_#_${key}`;
}
function isConnected(key: string): boolean {
return connections.some((c) => c.targetHandle === getEntryKey(key));
}
return (
<div className={cn(className, "flex flex-col")}>
{displayName && <strong>{displayName}</strong>}
<div>
{keyValuePairs.map(({ key, value }, index) => (
<div key={index}>
<div className="flex items-center space-x-2 mb-2 nodrag">
<Input
type="text"
placeholder="Key"
value={key}
onChange={(e) =>
updateKeyValuePairs(
keyValuePairs.toSpliced(index, 1, {
key: e.target.value,
value: value,
}),
)
}
{key && (
<NodeHandle
keyName={getEntryKey(key)}
schema={{ type: "string" }}
isConnected={isConnected(key)}
isRequired={false}
side="left"
/>
<Input
type="text"
placeholder="Value"
value={value ?? ""}
onChange={(e) =>
updateKeyValuePairs(
keyValuePairs.toSpliced(index, 1, {
key: key,
value: convertValueType(e.target.value),
}),
)
}
/>
<Button
variant="ghost"
className="px-2"
onClick={() =>
updateKeyValuePairs(keyValuePairs.toSpliced(index, 1))
}
>
<Cross2Icon />
</Button>
</div>
)}
{!isConnected(key) && (
<div className="nodrag mb-2 flex items-center space-x-2">
<Input
type="text"
placeholder="Key"
value={key}
onChange={(e) =>
updateKeyValuePairs(
keyValuePairs.toSpliced(index, 1, {
key: e.target.value,
value: value,
}),
)
}
/>
<Input
type="text"
placeholder="Value"
value={value ?? ""}
onBlur={(e) =>
updateKeyValuePairs(
keyValuePairs.toSpliced(index, 1, {
key: key,
value: convertValueType(e.target.value),
}),
)
}
/>
<Button
variant="ghost"
className="px-2"
onClick={() =>
updateKeyValuePairs(keyValuePairs.toSpliced(index, 1))
}
>
<Cross2Icon />
</Button>
</div>
)}
{errors[`${selfKey}.${key}`] && (
<span className="error-message">
{errors[`${selfKey}.${key}`]}
@ -368,6 +429,7 @@ const NodeArrayInput: FC<{
schema: BlockIOArraySubSchema;
entries?: string[];
errors: { [key: string]: string | undefined };
connections: NodeObjectInputTreeProps["connections"];
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
handleInputClick: NodeObjectInputTreeProps["handleInputClick"];
className?: string;
@ -377,6 +439,7 @@ const NodeArrayInput: FC<{
schema,
entries,
errors,
connections,
handleInputChange,
handleInputClick,
className,
@ -390,39 +453,52 @@ const NodeArrayInput: FC<{
<div className={cn(className, "flex flex-col")}>
{displayName && <strong>{displayName}</strong>}
{entries.map((entry: any, index: number) => {
const entryKey = `${selfKey}[${index}]`;
const entryKey = `${selfKey}_$_${index}`;
const isConnected =
connections && connections.some((c) => c.targetHandle === entryKey);
return (
<div key={entryKey}>
<div className="flex items-center space-x-2 mb-2">
{schema.items ? (
<NodeGenericInputField
propKey={entryKey}
propSchema={schema.items}
currentValue={entry}
errors={errors}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
/>
) : (
<NodeFallbackInput
selfKey={entryKey}
schema={schema.items}
value={entry}
error={errors[entryKey]}
displayName={displayName || beautifyString(selfKey)}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
/>
<div key={entryKey} className="self-start">
<div className="mb-2 flex space-x-2">
<NodeHandle
keyName={entryKey}
schema={schema.items!}
isConnected={isConnected}
isRequired={false}
side="left"
/>
{!isConnected &&
(schema.items ? (
<NodeGenericInputField
propKey={entryKey}
propSchema={schema.items}
currentValue={entry}
errors={errors}
connections={connections}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
/>
) : (
<NodeFallbackInput
selfKey={entryKey}
schema={schema.items}
value={entry}
error={errors[entryKey]}
displayName={displayName || beautifyString(selfKey)}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
/>
))}
{!isConnected && (
<Button
variant="ghost"
size="icon"
onClick={() =>
handleInputChange(selfKey, entries.toSpliced(index, 1))
}
>
<Cross2Icon />
</Button>
)}
<Button
variant="ghost"
size="icon"
onClick={() =>
handleInputChange(selfKey, entries.toSpliced(index, 1))
}
>
<Cross2Icon />
</Button>
</div>
{errors[entryKey] && typeof errors[entryKey] === "string" && (
<span className="error-message">{errors[entryKey]}</span>
@ -454,7 +530,7 @@ const NodeStringInput: FC<{
}> = ({
selfKey,
schema,
value,
value = "",
error,
handleInputChange,
handleInputClick,
@ -492,7 +568,7 @@ const NodeStringInput: FC<{
placeholder={
schema?.placeholder || `Enter ${beautifyString(displayName)}`
}
onChange={(e) => handleInputChange(selfKey, e.target.value)}
onBlur={(e) => handleInputChange(selfKey, e.target.value)}
className="pr-8 read-only:cursor-pointer read-only:text-gray-500"
/>
<Button
@ -511,6 +587,52 @@ const NodeStringInput: FC<{
);
};
export const NodeTextBoxInput: FC<{
selfKey: string;
schema: BlockIOStringSubSchema;
value?: string;
error?: string;
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
handleInputClick: NodeObjectInputTreeProps["handleInputClick"];
className?: string;
displayName: string;
}> = ({
selfKey,
schema,
value = "",
error,
handleInputChange,
handleInputClick,
className,
displayName,
}) => {
return (
<div className={className}>
<div
className="nodrag relative m-0 h-[200px] w-full bg-yellow-100 p-4"
onClick={schema.secret ? () => handleInputClick(selfKey) : undefined}
>
<textarea
id={selfKey}
value={schema.secret && value ? "********" : value}
readOnly={schema.secret}
placeholder={
schema?.placeholder || `Enter ${beautifyString(displayName)}`
}
onChange={(e) => handleInputChange(selfKey, e.target.value)}
onBlur={(e) => handleInputChange(selfKey, e.target.value)}
className="h-full w-full resize-none overflow-hidden border-none bg-transparent text-lg text-black outline-none"
style={{
fontSize: "min(1em, 16px)",
lineHeight: "1.2",
}}
/>
</div>
{error && <span className="error-message">{error}</span>}
</div>
);
};
const NodeNumberInput: FC<{
selfKey: string;
schema: BlockIONumberSubSchema;
@ -532,14 +654,12 @@ const NodeNumberInput: FC<{
displayName ??= schema.title || beautifyString(selfKey);
return (
<div className={className}>
<div className="flex items-center justify-between space-x-3 nodrag">
<div className="nodrag flex items-center justify-between space-x-3">
<Input
type="number"
id={selfKey}
value={value}
onChange={(e) =>
handleInputChange(selfKey, parseFloat(e.target.value))
}
onBlur={(e) => handleInputChange(selfKey, parseFloat(e.target.value))}
placeholder={
schema.placeholder || `Enter ${beautifyString(displayName)}`
}
@ -570,7 +690,7 @@ const NodeBooleanInput: FC<{
value ??= schema.default ?? false;
return (
<div className={className}>
<div className="flex items-center nodrag">
<div className="nodrag flex items-center">
<Switch
checked={value}
onCheckedChange={(v) => handleInputChange(selfKey, v)}

View File

@ -0,0 +1,501 @@
import Shepherd from "shepherd.js";
import "shepherd.js/dist/css/shepherd.css";
export const startTutorial = (
setPinBlocksPopover: (value: boolean) => void,
) => {
const tour = new Shepherd.Tour({
useModalOverlay: true,
defaultStepOptions: {
cancelIcon: { enabled: true },
scrollTo: { behavior: "smooth", block: "center" },
},
});
// CSS classes for disabling and highlighting blocks
const disableClass = "disable-blocks";
const highlightClass = "highlight-block";
let isConnecting = false;
// Helper function to disable all blocks except the target block
const disableOtherBlocks = (targetBlockSelector: string) => {
document
.querySelectorAll('[data-id^="add-block-button"]')
.forEach((block) => {
block.classList.toggle(
disableClass,
!block.matches(targetBlockSelector),
);
block.classList.toggle(
highlightClass,
block.matches(targetBlockSelector),
);
});
};
// Helper function to enable all blocks
const enableAllBlocks = () => {
document
.querySelectorAll('[data-id^="add-block-button"]')
.forEach((block) => {
block.classList.remove(disableClass, highlightClass);
});
};
// Inject CSS for disabling and highlighting blocks
const injectStyles = () => {
const style = document.createElement("style");
style.textContent = `
.${disableClass} {
pointer-events: none;
opacity: 0.5;
}
.${highlightClass} {
background-color: #ffeb3b;
border: 2px solid #fbc02d;
transition: background-color 0.3s, border-color 0.3s;
}
`;
document.head.appendChild(style);
};
// Helper function to check if an element is present in the DOM
const waitForElement = (selector: string): Promise<void> => {
return new Promise((resolve) => {
const checkElement = () => {
if (document.querySelector(selector)) {
resolve();
} else {
setTimeout(checkElement, 10);
}
};
checkElement();
});
};
// Function to detect the correct connection and advance the tour
const detectConnection = () => {
const checkForConnection = () => {
const correctConnection = document.querySelector(
'[data-testid="rf__edge-1_result_2_a"]',
);
if (correctConnection) {
tour.show("press-run-again");
} else {
setTimeout(checkForConnection, 100);
}
};
checkForConnection();
};
// Define state management functions to handle connection state
function startConnecting() {
isConnecting = true;
}
function stopConnecting() {
isConnecting = false;
}
// Reset connection state when revisiting the step
function resetConnectionState() {
stopConnecting();
}
// Event handlers for mouse down and up to manage connection state
function handleMouseDown() {
startConnecting();
setTimeout(() => {
if (isConnecting) {
tour.next();
}
}, 100);
}
// Event handler for mouse up to check if the connection was successful
function handleMouseUp(event: { target: any }) {
const target = event.target;
const validConnectionPoint = document.querySelector(
'[data-id="2-a-target"]',
);
if (validConnectionPoint && !validConnectionPoint.contains(target)) {
setTimeout(() => {
if (!document.querySelector('[data-testid="rf__edge-1_result_2_a"]')) {
stopConnecting();
tour.show("connect-blocks-output");
}
}, 200);
} else {
stopConnecting();
}
}
// Define the fitViewToScreen function
const fitViewToScreen = () => {
const fitViewButton = document.querySelector(
".react-flow__controls-fitview",
) as HTMLButtonElement;
if (fitViewButton) {
fitViewButton.click();
}
};
injectStyles();
tour.addStep({
id: "starting-step",
title: "Welcome to the Tutorial",
text: "This is the AutoGPT builder!",
buttons: [
{
text: "Skip Tutorial",
action: () => {
tour.cancel(); // Ends the tour
localStorage.setItem("shepherd-tour", "skipped"); // Set the tutorial as skipped in local storage
},
classes: "shepherd-button-secondary", // Optionally add a class for styling the skip button differently
},
{
text: "Next",
action: tour.next,
},
],
});
tour.addStep({
id: "open-block-step",
title: "Open Blocks Menu",
text: "Please click the block button to open the blocks menu.",
attachTo: {
element: '[data-id="blocks-control-popover-trigger"]',
on: "bottom",
},
advanceOn: {
selector: '[data-id="blocks-control-popover-trigger"]',
event: "click",
},
buttons: [],
});
tour.addStep({
id: "scroll-block-menu",
title: "Scroll Down or Search",
text: 'Scroll down or search in the blocks menu for the "Calculator Block" and press the "+" to add the block.',
attachTo: {
element: '[data-id="blocks-control-popover-content"]',
on: "right",
},
buttons: [],
beforeShowPromise: () =>
waitForElement('[data-id="blocks-control-popover-content"]').then(() => {
disableOtherBlocks(
'[data-id="add-block-button-b1ab9b19-67a6-406d-abf5-2dba76d00c79"]',
);
}),
advanceOn: {
selector:
'[data-id="add-block-button-b1ab9b19-67a6-406d-abf5-2dba76d00c79"]',
event: "click",
},
when: {
show: () => setPinBlocksPopover(true),
hide: enableAllBlocks,
},
});
tour.addStep({
id: "focus-new-block",
title: "New Block",
text: "This is the Calculator Block! Let's go over how it works.",
attachTo: { element: `[data-id="custom-node-1"]`, on: "left" },
beforeShowPromise: () => waitForElement('[data-id="custom-node-1"]'),
buttons: [
{
text: "Next",
action: tour.next,
},
],
when: {
show: () => {
setPinBlocksPopover(false);
fitViewToScreen();
},
},
});
tour.addStep({
id: "input-to-block",
title: "Input to the Block",
text: "This is the input pin for the block. You can input the output of other blocks here; this block takes numbers as input.",
attachTo: { element: '[data-nodeid="1"]', on: "left" },
buttons: [
{
text: "Back",
action: tour.back,
},
{
text: "Next",
action: tour.next,
},
],
});
tour.addStep({
id: "output-from-block",
title: "Output from the Block",
text: "This is the output pin for the block. You can connect this to another block to pass the output along.",
attachTo: { element: '[data-handlepos="right"]', on: "right" },
buttons: [
{
text: "Back",
action: tour.back,
},
{
text: "Next",
action: tour.next,
},
],
});
tour.addStep({
id: "select-operation",
title: "Select Operation",
text: 'Select a mathematical operation to perform. Lets choose "Add" for now.',
attachTo: { element: ".mt-1.mb-2", on: "right" },
buttons: [
{
text: "Back",
action: tour.back,
},
{
text: "Next",
action: tour.next,
},
],
when: {
show: () => tour.modal.hide(),
hide: () => tour.modal.show(),
},
});
tour.addStep({
id: "enter-number-1",
title: "Enter a Number",
text: "Enter a number here to try the Calculator Block!",
attachTo: { element: "#a", on: "right" },
buttons: [
{
text: "Back",
action: tour.back,
},
{
text: "Next",
action: tour.next,
},
],
});
tour.addStep({
id: "enter-number-2",
title: "Enter Another Number",
text: "Enter another number here!",
attachTo: { element: "#b", on: "right" },
buttons: [
{
text: "Back",
action: tour.back,
},
{
text: "Next",
action: tour.next,
},
],
});
tour.addStep({
id: "press-run",
title: "Press Run",
text: "Start your first flow by pressing the Run button!",
attachTo: { element: '[data-id="control-button-2"]', on: "right" },
advanceOn: { selector: '[data-id="control-button-2"]', event: "click" },
buttons: [
{
text: "Back",
action: tour.back,
},
],
});
tour.addStep({
id: "wait-for-processing",
title: "Processing",
text: "Let's wait for the block to finish being processed...",
attachTo: { element: '[data-id="badge-1-QUEUED"]', on: "bottom" },
buttons: [],
beforeShowPromise: () => waitForElement('[data-id="badge-1-QUEUED"]'),
when: {
show: () => {
fitViewToScreen();
waitForElement('[data-id="badge-1-COMPLETED"]').then(() => {
tour.next();
});
},
},
});
tour.addStep({
id: "check-output",
title: "Check the Output",
text: "Check here to see the output of the block after running the flow.",
attachTo: { element: '[data-id="latest-output"]', on: "bottom" },
beforeShowPromise: () => waitForElement('[data-id="latest-output"]'),
buttons: [
{
text: "Back",
action: tour.back,
},
{
text: "Next",
action: tour.next,
},
],
when: {
show: () => {
fitViewToScreen();
},
},
});
tour.addStep({
id: "copy-paste-block",
title: "Copy and Paste the Block",
text: "Lets duplicate this block. Click and hold the block with your mouse, then press Ctrl+C (Cmd+C on Mac) to copy and Ctrl+V (Cmd+V on Mac) to paste.",
attachTo: { element: `[data-id="custom-node-1"]`, on: "top" },
buttons: [
{
text: "Back",
action: tour.back,
},
],
when: {
show: () => {
fitViewToScreen();
waitForElement('[data-id="custom-node-2"]').then(() => {
tour.next();
});
},
},
});
tour.addStep({
id: "focus-second-block",
title: "Focus on the New Block",
text: "This is your copied Calculator Block. Now, lets move it to the side of the first block.",
attachTo: { element: `[data-id="custom-node-2"]`, on: "top" },
beforeShowPromise: () => waitForElement('[data-id="custom-node-2"]'),
buttons: [
{
text: "Next",
action: tour.next,
},
],
});
tour.addStep({
id: "connect-blocks-output",
title: "Connect the Blocks: Output",
text: "Now, lets connect the output of the first Calculator Block to the input of the second Calculator Block. Drag from the output pin of the first block to the input pin (A) of the second block.",
attachTo: { element: '[data-id="1-1-result-source"]', on: "bottom" },
buttons: [
{
text: "Back",
action: tour.back,
},
],
beforeShowPromise: () => {
return waitForElement('[data-id="1-1-result-source"]');
},
when: {
show: () => {
fitViewToScreen();
resetConnectionState(); // Reset state when revisiting this step
tour.modal.show();
const outputPin = document.querySelector(
'[data-id="1-1-result-source"]',
);
if (outputPin) {
outputPin.addEventListener("mousedown", handleMouseDown);
}
},
hide: () => {
const outputPin = document.querySelector(
'[data-id="1-1-result-source"]',
);
if (outputPin) {
outputPin.removeEventListener("mousedown", handleMouseDown);
}
},
},
});
tour.addStep({
id: "connect-blocks-input",
title: "Connect the Blocks: Input",
text: "Now, connect the output to the input pin of the second block (A).",
attachTo: { element: '[data-id="1-2-a-target"]', on: "top" },
buttons: [],
beforeShowPromise: () => {
return waitForElement('[data-id="1-2-a-target"]').then(() => {
detectConnection();
});
},
when: {
show: () => {
tour.modal.show();
document.addEventListener("mouseup", handleMouseUp, true);
},
hide: () => {
tour.modal.hide();
document.removeEventListener("mouseup", handleMouseUp, true);
},
},
});
tour.addStep({
id: "press-run-again",
title: "Press Run Again",
text: "Now, press the Run button again to execute the flow with the new Calculator Block added!",
attachTo: { element: '[data-id="control-button-2"]', on: "right" },
advanceOn: { selector: '[data-id="control-button-2"]', event: "click" },
buttons: [],
});
tour.addStep({
id: "congratulations",
title: "Congratulations!",
text: "You have successfully created your first flow. Watch for the outputs in the blocks!",
beforeShowPromise: () => waitForElement('[data-id="latest-output"]'),
when: {
show: () => tour.modal.hide(),
},
buttons: [
{
text: "Finish",
action: tour.complete,
},
],
});
// Unpin blocks when the tour is completed or canceled
tour.on("complete", () => {
setPinBlocksPopover(false);
localStorage.setItem("shepherd-tour", "completed"); // Optionally mark the tutorial as completed
});
tour.on("cancel", () => {
setPinBlocksPopover(false);
localStorage.setItem("shepherd-tour", "canceled"); // Optionally mark the tutorial as canceled
});
tour.start();
};

View File

@ -0,0 +1,60 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border border-neutral-200 px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-neutral-950 [&>svg~*]:pl-7 dark:border-neutral-800 dark:[&>svg]:text-neutral-50",
{
variants: {
variant: {
default:
"bg-white text-neutral-950 dark:bg-neutral-950 dark:text-neutral-50",
destructive:
"border-red-500/50 text-red-500 dark:border-red-500 [&>svg]:text-red-500 dark:border-red-900/50 dark:text-red-900 dark:dark:border-red-900 dark:[&>svg]:text-red-900",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
));
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
));
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription };

View File

@ -0,0 +1,30 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { CheckIcon } from "@radix-ui/react-icons";
import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-neutral-200 border-neutral-900 shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=checked]:text-neutral-50 dark:border-neutral-50 dark:border-neutral-800 dark:focus-visible:ring-neutral-300 dark:data-[state=checked]:bg-neutral-50 dark:data-[state=checked]:text-neutral-900",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };

View File

@ -0,0 +1,155 @@
"use client";
import * as React from "react";
import { type DialogProps } from "@radix-ui/react-dialog";
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
import { Command as CommandPrimitive } from "cmdk";
import { cn } from "@/lib/utils";
import { Dialog, DialogContent } from "@/components/ui/dialog";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-white text-neutral-950 dark:bg-neutral-950 dark:text-neutral-50",
className,
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-neutral-500 dark:[&_[cmdk-group-heading]]:text-neutral-400 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<MagnifyingGlassIcon className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-neutral-500 disabled:cursor-not-allowed disabled:opacity-50 dark:placeholder:text-neutral-400",
className,
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-neutral-950 dark:text-neutral-50 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-neutral-500 dark:[&_[cmdk-group-heading]]:text-neutral-400",
className,
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-neutral-200 dark:bg-neutral-800", className)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-neutral-100 data-[selected=true]:text-neutral-900 data-[disabled=true]:opacity-50 dark:data-[selected=true]:bg-neutral-800 dark:data-[selected=true]:text-neutral-50",
className,
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-neutral-500 dark:text-neutral-400",
className,
)}
{...props}
/>
);
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};

View File

@ -0,0 +1,209 @@
"use client";
import {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
useReactTable,
getPaginationRowModel,
getSortedRowModel,
getFilteredRowModel,
} from "@tanstack/react-table";
import {
TableHeader,
TableRow,
TableHead,
TableBody,
TableCell,
Table,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { cloneElement, Fragment, useState } from "react";
export interface GlobalActions<TData> {
component: React.ReactElement;
action: (rows: TData[]) => Promise<void>;
}
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
filterPlaceholder: string;
filterColumn?: string;
globalActions?: GlobalActions<TData>[];
}
export function DataTable<TData, TValue>({
columns,
data,
filterPlaceholder = "Filter...",
filterColumn,
globalActions = [],
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [rowSelection, setRowSelection] = useState({});
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
});
return (
<div>
<div className="flex items-center gap-2 py-4">
{filterColumn && (
<Input
placeholder={filterPlaceholder}
value={
(table.getColumn(filterColumn)?.getFilterValue() as string) ?? ""
}
onChange={(event) =>
table.getColumn(filterColumn)?.setFilterValue(event.target.value)
}
className="max-w-sm"
/>
)}
{globalActions &&
globalActions.map((action, index) => {
return (
<Fragment key={index}>
<div className="flex items-center">
{cloneElement(action.component, {
onClick: () => {
const filteredSelectedRows = table
.getFilteredSelectedRowModel()
.rows.map((row) => row.original);
action.action(filteredSelectedRows);
},
})}
</div>
</Fragment>
);
})}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto">
Columns
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
);
}

View File

@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
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",
"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,
)}
{...props}
@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
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",
"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%] dark:border-neutral-800 dark:bg-neutral-950 sm:rounded-lg",
className,
)}
{...props}

View File

@ -405,6 +405,40 @@ export const IconPlay = createIcon((props) => (
</svg>
));
/**
* Square icon component.
*
* @component IconSquare
* @param {IconProps} props - The props object containing additional attributes and event handlers for the icon.
* @returns {JSX.Element} - The square icon.
*
* @example
* // Default usage this is the standard usage
* <IconSquare />
*
* @example
* // With custom color and size these should be used sparingly and only when necessary
* <IconSquare className="text-primary" size="lg" />
*
* @example
* // With custom size and onClick handler
* <IconSquare size="sm" onClick={handleOnClick} />
*/
export const IconSquare = createIcon((props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<rect width="18" height="18" x="3" y="3" rx="2" />
</svg>
));
/**
* Package2 icon component.
*

View File

@ -6,16 +6,30 @@ export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
({ className, type, value, ...props }, ref) => {
// This ref allows the `Input` component to be both controlled and uncontrolled.
// The HTMLvalue will only be updated if the value prop changes, but the user can still type in the input.
ref = ref || React.createRef<HTMLInputElement>();
React.useEffect(() => {
if (
ref &&
ref.current &&
ref.current.value !== value &&
type !== "file"
) {
ref.current.value = value;
}
}, [value, type, ref]);
return (
<input
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
"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-400 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-800 dark:placeholder:text-gray-400 dark:focus-visible:ring-gray-300",
type == "file" ? "pb-0.5 pt-1.5" : "", // fix alignment
className,
)}
ref={ref}
defaultValue={type !== "file" ? value : undefined}
{...props}
/>
);

View File

@ -0,0 +1,318 @@
"use client";
import { Badge } from "@/components/ui/badge";
import {
Command,
CommandItem,
CommandEmpty,
CommandList,
} from "@/components/ui/command";
import { cn } from "@/lib/utils";
import { Command as CommandPrimitive } from "cmdk";
import { X as RemoveIcon, Check } from "lucide-react";
import React, {
KeyboardEvent,
createContext,
forwardRef,
useCallback,
useContext,
useState,
} from "react";
type MultiSelectorProps = {
values: string[];
onValuesChange: (value: string[]) => void;
loop?: boolean;
} & React.ComponentPropsWithoutRef<typeof CommandPrimitive>;
interface MultiSelectContextProps {
value: string[];
onValueChange: (value: any) => void;
open: boolean;
setOpen: (value: boolean) => void;
inputValue: string;
setInputValue: React.Dispatch<React.SetStateAction<string>>;
activeIndex: number;
setActiveIndex: React.Dispatch<React.SetStateAction<number>>;
}
const MultiSelectContext = createContext<MultiSelectContextProps | null>(null);
const useMultiSelect = () => {
const context = useContext(MultiSelectContext);
if (!context) {
throw new Error("useMultiSelect must be used within MultiSelectProvider");
}
return context;
};
const MultiSelector = forwardRef<HTMLDivElement, MultiSelectorProps>(
(
{
values: value,
onValuesChange: onValueChange,
loop = false,
className,
children,
dir,
...props
},
ref,
) => {
const [inputValue, setInputValue] = useState("");
const [open, setOpen] = useState<boolean>(false);
const [activeIndex, setActiveIndex] = useState<number>(-1);
const onValueChangeHandler = useCallback(
(val: string) => {
if (value.includes(val)) {
onValueChange(value.filter((item) => item !== val));
} else {
onValueChange([...value, val]);
}
},
[value, onValueChange],
);
const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLDivElement>) => {
const moveNext = () => {
const nextIndex = activeIndex + 1;
setActiveIndex(
nextIndex > value.length - 1 ? (loop ? 0 : -1) : nextIndex,
);
};
const movePrev = () => {
const prevIndex = activeIndex - 1;
setActiveIndex(prevIndex < 0 ? value.length - 1 : prevIndex);
};
if ((e.key === "Backspace" || e.key === "Delete") && value.length > 0) {
if (inputValue.length === 0) {
if (activeIndex !== -1 && activeIndex < value.length) {
onValueChange(
value.filter((item) => item !== value[activeIndex]),
);
const newIndex = activeIndex - 1 < 0 ? 0 : activeIndex - 1;
setActiveIndex(newIndex);
} else {
onValueChange(
value.filter((item) => item !== value[value.length - 1]),
);
}
}
} else if (e.key === "Enter") {
setOpen(true);
} else if (e.key === "Escape") {
if (activeIndex !== -1) {
setActiveIndex(-1);
} else {
setOpen(false);
}
} else if (dir === "rtl") {
if (e.key === "ArrowRight") {
movePrev();
} else if (e.key === "ArrowLeft" && (activeIndex !== -1 || loop)) {
moveNext();
}
} else {
if (e.key === "ArrowLeft") {
movePrev();
} else if (e.key === "ArrowRight" && (activeIndex !== -1 || loop)) {
moveNext();
}
}
},
[value, inputValue, activeIndex, loop, onValueChange, dir],
);
return (
<MultiSelectContext.Provider
value={{
value,
onValueChange: onValueChangeHandler,
open,
setOpen,
inputValue,
setInputValue,
activeIndex,
setActiveIndex,
}}
>
<Command
ref={ref}
onKeyDown={handleKeyDown}
className={cn(
"flex flex-col space-y-2 overflow-visible bg-transparent",
className,
)}
dir={dir}
{...props}
>
{children}
</Command>
</MultiSelectContext.Provider>
);
},
);
MultiSelector.displayName = "MultiSelector";
const MultiSelectorTrigger = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, children, ...props }, ref) => {
const { value, onValueChange, activeIndex } = useMultiSelect();
const mousePreventDefault = useCallback((e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
}, []);
return (
<div
ref={ref}
className={cn(
"flex flex-wrap gap-1 rounded-lg border border-muted bg-background p-1 py-2",
className,
)}
{...props}
>
{value.map((item, index) => (
<Badge
key={item}
className={cn(
"flex items-center gap-1 rounded-xl px-1",
activeIndex === index && "ring-2 ring-muted-foreground",
)}
variant={"secondary"}
>
<span className="text-xs">{item}</span>
<button
aria-label={`Remove ${item} option`}
aria-roledescription="button to remove option"
type="button"
onMouseDown={mousePreventDefault}
onClick={() => onValueChange(item)}
>
<span className="sr-only">Remove {item} option</span>
<RemoveIcon className="h-4 w-4 hover:stroke-destructive" />
</button>
</Badge>
))}
{children}
</div>
);
});
MultiSelectorTrigger.displayName = "MultiSelectorTrigger";
const MultiSelectorInput = forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => {
const { setOpen, inputValue, setInputValue, activeIndex, setActiveIndex } =
useMultiSelect();
return (
<CommandPrimitive.Input
{...props}
ref={ref}
value={inputValue}
onValueChange={activeIndex === -1 ? setInputValue : undefined}
onBlur={() => setOpen(false)}
onFocus={() => setOpen(true)}
onClick={() => setActiveIndex(-1)}
className={cn(
"ml-2 flex-1 bg-transparent outline-none placeholder:text-muted-foreground",
className,
activeIndex !== -1 && "caret-transparent",
)}
/>
);
});
MultiSelectorInput.displayName = "MultiSelectorInput";
const MultiSelectorContent = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ children }, ref) => {
const { open } = useMultiSelect();
return (
<div ref={ref} className="relative">
{open && children}
</div>
);
});
MultiSelectorContent.displayName = "MultiSelectorContent";
const MultiSelectorList = forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, children }, ref) => {
return (
<CommandList
ref={ref}
className={cn(
"scrollbar-thin scrollbar-track-transparent scrollbar-thumb-muted-foreground dark:scrollbar-thumb-muted scrollbar-thumb-rounded-lg absolute top-0 z-10 flex w-full flex-col gap-2 rounded-md border border-muted bg-background p-2 shadow-md transition-colors",
className,
)}
>
{children}
<CommandEmpty>
<span className="text-muted-foreground">No results found</span>
</CommandEmpty>
</CommandList>
);
});
MultiSelectorList.displayName = "MultiSelectorList";
const MultiSelectorItem = forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
{ value: string } & React.ComponentPropsWithoutRef<
typeof CommandPrimitive.Item
>
>(({ className, value, children, ...props }, ref) => {
const { value: Options, onValueChange, setInputValue } = useMultiSelect();
const mousePreventDefault = useCallback((e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
}, []);
const isIncluded = Options.includes(value);
return (
<CommandItem
ref={ref}
{...props}
onSelect={() => {
onValueChange(value);
setInputValue("");
}}
className={cn(
"flex cursor-pointer justify-between rounded-md px-2 py-1 transition-colors",
className,
isIncluded && "cursor-default opacity-50",
props.disabled && "cursor-not-allowed opacity-50",
)}
onMouseDown={mousePreventDefault}
>
{children}
{isIncluded && <Check className="h-4 w-4" />}
</CommandItem>
);
});
MultiSelectorItem.displayName = "MultiSelectorItem";
export {
MultiSelector,
MultiSelectorTrigger,
MultiSelectorInput,
MultiSelectorContent,
MultiSelectorList,
MultiSelectorItem,
};

View File

@ -24,7 +24,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-neutral-200 bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-white placeholder:text-neutral-500 focus:outline-none focus:ring-1 focus:ring-neutral-950 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 dark:border-neutral-800 dark:ring-offset-neutral-950 dark:placeholder:text-neutral-400 dark:focus:ring-neutral-300",
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-neutral-200 bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-white placeholder:text-neutral-500 focus:outline-none focus:ring-1 focus:ring-neutral-950 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-800 dark:ring-offset-neutral-950 dark:placeholder:text-neutral-400 dark:focus:ring-neutral-300 [&>span]:line-clamp-1",
className,
)}
{...props}

View File

@ -21,7 +21,7 @@ const SheetOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
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",
"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,
)}
{...props}

View File

@ -43,7 +43,7 @@ const TableFooter = React.forwardRef<
<tfoot
ref={ref}
className={cn(
"border-t bg-neutral-100/50 font-medium [&>tr]:last:border-b-0 dark:bg-neutral-800/50",
"border-t bg-neutral-100/50 font-medium dark:bg-neutral-800/50 [&>tr]:last:border-b-0",
className,
)}
{...props}
@ -73,7 +73,7 @@ const TableHead = React.forwardRef<
<th
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",
"h-10 px-2 text-left align-middle font-medium text-neutral-500 dark:text-neutral-400 [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}

View File

@ -0,0 +1,130 @@
"use client";
import * as React from "react";
import { Cross2Icon } from "@radix-ui/react-icons";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className,
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border border-neutral-200 p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full dark:border-neutral-800",
{
variants: {
variant: {
default:
"border bg-white text-neutral-950 dark:bg-neutral-950 dark:text-neutral-50",
destructive:
"destructive group border-red-500 bg-red-500 text-neutral-50 dark:border-red-900 dark:bg-red-900 dark:text-neutral-50",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border border-neutral-200 bg-transparent px-3 text-sm font-medium transition-colors hover:bg-neutral-100 focus:outline-none focus:ring-1 focus:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-neutral-100/40 group-[.destructive]:hover:border-red-500/30 group-[.destructive]:hover:bg-red-500 group-[.destructive]:hover:text-neutral-50 group-[.destructive]:focus:ring-red-500 dark:border-neutral-800 dark:hover:bg-neutral-800 dark:focus:ring-neutral-300 dark:group-[.destructive]:border-neutral-800/40 dark:group-[.destructive]:hover:border-red-900/30 dark:group-[.destructive]:hover:bg-red-900 dark:group-[.destructive]:hover:text-neutral-50 dark:group-[.destructive]:focus:ring-red-900",
className,
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-1 top-1 rounded-md p-1 text-neutral-950/50 opacity-0 transition-opacity hover:text-neutral-950 focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 dark:text-neutral-50/50 dark:hover:text-neutral-50",
className,
)}
toast-close=""
{...props}
>
<Cross2Icon className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
{...props}
/>
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
};

View File

@ -0,0 +1,35 @@
"use client";
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast";
import { useToast } from "@/components/ui/use-toast";
export function Toaster() {
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}

View File

@ -0,0 +1,191 @@
"use client";
// Inspired by react-hot-toast library
import * as React from "react";
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 1000000;
type ToasterToast = ToastProps & {
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const;
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString();
}
type ActionType = typeof actionTypes;
type Action =
| {
type: ActionType["ADD_TOAST"];
toast: ToasterToast;
}
| {
type: ActionType["UPDATE_TOAST"];
toast: Partial<ToasterToast>;
}
| {
type: ActionType["DISMISS_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
};
interface State {
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
});
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t,
),
};
case "DISMISS_TOAST": {
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t,
),
};
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
};
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] };
function dispatch(action: Action) {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}
type Toast = Omit<ToasterToast, "id">;
function toast({ ...props }: Toast) {
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
},
},
});
return {
id: id,
dismiss,
update,
};
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
export { useToast, toast };

View File

@ -8,13 +8,26 @@ const getServerUser = async () => {
}
try {
const { data, error } = await supabase.auth.getUser();
const {
data: { user },
error,
} = await supabase.auth.getUser();
if (error) {
return { user: null, error: error.message };
console.error("Supabase auth error:", error);
return { user: null, role: null, error: `Auth error: ${error.message}` };
}
return { user: data.user, error: null };
if (!user) {
return { user: null, role: null, error: "No user found in the response" };
}
const role = user.role || null;
return { user, role, error: null };
} catch (error) {
return { user: null, error: (error as Error).message };
console.error("Unexpected error in getServerUser:", error);
return {
user: null,
role: null,
error: `Unexpected error: ${(error as Error).message}`,
};
}
};

View File

@ -0,0 +1,776 @@
import { CustomEdge } from "@/components/CustomEdge";
import { CustomNode } from "@/components/CustomNode";
import AutoGPTServerAPI, {
Block,
BlockIOSubSchema,
Graph,
Link,
NodeExecutionResult,
} from "@/lib/autogpt-server-api";
import {
deepEquals,
getTypeColor,
removeEmptyStringsAndNulls,
setNestedProperty,
} from "@/lib/utils";
import { Connection, MarkerType } from "@xyflow/react";
import Ajv from "ajv";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
const ajv = new Ajv({ strict: false, allErrors: true });
export default function useAgentGraph(
flowID?: string,
template?: boolean,
passDataToBeads?: boolean,
) {
const [savedAgent, setSavedAgent] = useState<Graph | null>(null);
const [agentDescription, setAgentDescription] = useState<string>("");
const [agentName, setAgentName] = useState<string>("");
const [availableNodes, setAvailableNodes] = useState<Block[]>([]);
const [updateQueue, setUpdateQueue] = useState<NodeExecutionResult[]>([]);
const processedUpdates = useRef<NodeExecutionResult[]>([]);
/**
* User `request` to save or save&run the agent, or to stop the active run.
* `state` is used to track the request status:
* - none: no request
* - saving: request was sent to save the agent
* and nodes are pending sync to update their backend ids
* - running: request was sent to run the agent
* and frontend is enqueueing execution results
* - stopping: a request to stop the active run has been sent; response is pending
* - error: request failed
*/
const [saveRunRequest, setSaveRunRequest] = useState<
| {
request: "none" | "save" | "run";
state: "none" | "saving" | "error";
}
| {
request: "run" | "stop";
state: "running" | "stopping" | "error";
activeExecutionID?: string;
}
>({
request: "none",
state: "none",
});
// Determines if nodes backend ids are synced with saved agent (actual ids on the backend)
const [nodesSyncedWithSavedAgent, setNodesSyncedWithSavedAgent] =
useState(false);
const [nodes, setNodes] = useState<CustomNode[]>([]);
const [edges, setEdges] = useState<CustomEdge[]>([]);
const api = useMemo(
() => new AutoGPTServerAPI(process.env.NEXT_PUBLIC_AGPT_SERVER_URL!),
[],
);
// Connect to WebSocket
useEffect(() => {
api
.connectWebSocket()
.then(() => {
console.debug("WebSocket connected");
api.onWebSocketMessage("execution_event", (data) => {
setUpdateQueue((prev) => [...prev, data]);
});
})
.catch((error) => {
console.error("Failed to connect WebSocket:", error);
});
return () => {
api.disconnectWebSocket();
};
}, [api]);
// Load available blocks
useEffect(() => {
api
.getBlocks()
.then((blocks) => setAvailableNodes(blocks))
.catch();
}, [api]);
//TODO to utils? repeated in Flow
const formatEdgeID = useCallback((conn: Link | Connection): string => {
if ("sink_id" in conn) {
return `${conn.source_id}_${conn.source_name}_${conn.sink_id}_${conn.sink_name}`;
} else {
return `${conn.source}_${conn.sourceHandle}_${conn.target}_${conn.targetHandle}`;
}
}, []);
const getOutputType = useCallback(
(nodes: CustomNode[], nodeId: string, handleId: string) => {
const node = nodes.find((n) => n.id === nodeId);
if (!node) return "unknown";
const outputSchema = node.data.outputSchema;
if (!outputSchema) return "unknown";
const outputHandle = outputSchema.properties[handleId];
if (!("type" in outputHandle)) return "unknown";
return outputHandle.type;
},
[],
);
// Load existing graph
const loadGraph = useCallback(
(graph: Graph) => {
setSavedAgent(graph);
setAgentName(graph.name);
setAgentDescription(graph.description);
setNodes(() => {
const newNodes = graph.nodes.map((node) => {
const block = availableNodes.find(
(block) => block.id === node.block_id,
)!;
const newNode: CustomNode = {
id: node.id,
type: "custom",
position: {
x: node.metadata.position.x,
y: node.metadata.position.y,
},
data: {
block_id: block.id,
blockType: block.name,
categories: block.categories,
description: block.description,
title: `${block.name} ${node.id}`,
inputSchema: block.inputSchema,
outputSchema: block.outputSchema,
hardcodedValues: node.input_default,
connections: graph.links
.filter((l) => [l.source_id, l.sink_id].includes(node.id))
.map((link) => ({
edge_id: formatEdgeID(link),
source: link.source_id,
sourceHandle: link.source_name,
target: link.sink_id,
targetHandle: link.sink_name,
})),
isOutputOpen: false,
},
};
return newNode;
});
setEdges((_) =>
graph.links.map((link) => ({
id: formatEdgeID(link),
type: "custom",
data: {
edgeColor: getTypeColor(
getOutputType(newNodes, link.source_id, link.source_name!),
),
sourcePos: newNodes.find((node) => node.id === link.source_id)
?.position,
isStatic: link.is_static,
beadUp: 0,
beadDown: 0,
beadData: [],
},
markerEnd: {
type: MarkerType.ArrowClosed,
strokeWidth: 2,
color: getTypeColor(
getOutputType(newNodes, link.source_id, link.source_name!),
),
},
source: link.source_id,
target: link.sink_id,
sourceHandle: link.source_name || undefined,
targetHandle: link.sink_name || undefined,
})),
);
return newNodes;
});
},
[availableNodes, formatEdgeID, getOutputType],
);
const getFrontendId = useCallback(
(backendId: string, nodes: CustomNode[]) => {
const node = nodes.find((node) => node.data.backend_id === backendId);
return node?.id;
},
[],
);
const updateEdgeBeads = useCallback(
(executionData: NodeExecutionResult) => {
setEdges((edges) => {
return edges.map((e) => {
const edge = { ...e, data: { ...e.data } } as CustomEdge;
if (executionData.status === "COMPLETED") {
// Produce output beads
for (let key in executionData.output_data) {
if (
edge.source !== getFrontendId(executionData.node_id, nodes) ||
edge.sourceHandle !== key
) {
continue;
}
edge.data!.beadUp = (edge.data!.beadUp ?? 0) + 1;
// For static edges beadDown is always one less than beadUp
// Because there's no queueing and one bead is always at the connection point
if (edge.data?.isStatic) {
edge.data!.beadDown = (edge.data!.beadUp ?? 0) - 1;
edge.data!.beadData = edge.data!.beadData!.slice(0, -1);
continue;
}
//todo kcze this assumes output at key is always array with one element
edge.data!.beadData = [
executionData.output_data[key][0],
...edge.data!.beadData!,
];
}
} else if (executionData.status === "RUNNING") {
// Consume input beads
for (let key in executionData.input_data) {
if (
edge.target !== getFrontendId(executionData.node_id, nodes) ||
edge.targetHandle !== key
) {
continue;
}
// Skip decreasing bead count if edge doesn't match or if it's static
if (
edge.data!.beadData![edge.data!.beadData!.length - 1] !==
executionData.input_data[key] ||
edge.data?.isStatic
) {
continue;
}
edge.data!.beadDown = (edge.data!.beadDown ?? 0) + 1;
edge.data!.beadData = edge.data!.beadData!.slice(0, -1);
}
}
return edge;
});
});
},
[getFrontendId, nodes],
);
const updateNodesWithExecutionData = useCallback(
(executionData: NodeExecutionResult) => {
if (passDataToBeads) {
updateEdgeBeads(executionData);
}
setNodes((nodes) => {
const nodeId = nodes.find(
(node) => node.data.backend_id === executionData.node_id,
)?.id;
if (!nodeId) {
console.error(
"Node not found for execution data:",
executionData,
"This shouldn't happen and means that the frontend and backend are out of sync.",
);
return nodes;
}
return nodes.map((node) =>
node.id === nodeId
? {
...node,
data: {
...node.data,
status: executionData.status,
executionResults:
Object.keys(executionData.output_data).length > 0
? [
...(node.data.executionResults || []),
{
execId: executionData.node_exec_id,
data: executionData.output_data,
},
]
: node.data.executionResults,
isOutputOpen: true,
},
}
: node,
);
});
},
[passDataToBeads, updateEdgeBeads],
);
useEffect(() => {
if (!flowID || availableNodes.length == 0) return;
(template ? api.getTemplate(flowID) : api.getGraph(flowID)).then(
(graph) => {
console.log("Loading graph");
loadGraph(graph);
},
);
}, [flowID, template, availableNodes, api, loadGraph]);
// Update nodes with execution data
useEffect(() => {
if (updateQueue.length === 0 || !nodesSyncedWithSavedAgent) {
return;
}
setUpdateQueue((prev) => {
prev.forEach((data) => {
// Skip already processed updates by checking
// if the data is in the processedUpdates array by reference
// This is not to process twice in react dev mode
// because it'll add double the beads
if (processedUpdates.current.includes(data)) {
return;
}
updateNodesWithExecutionData(data);
processedUpdates.current.push(data);
});
return [];
});
}, [updateQueue, nodesSyncedWithSavedAgent, updateNodesWithExecutionData]);
const validateNodes = useCallback((): boolean => {
let isValid = true;
nodes.forEach((node) => {
const validate = ajv.compile(node.data.inputSchema);
const errors = {} as { [key: string]: string };
// Validate values against schema using AJV
const valid = validate(node.data.hardcodedValues);
if (!valid) {
// Populate errors if validation fails
validate.errors?.forEach((error) => {
// Skip error if there's an edge connected
const path =
"dataPath" in error
? (error.dataPath as string)
: error.instancePath;
const handle = path.split(/[\/.]/)[0];
if (
node.data.connections.some(
(conn) => conn.target === node.id || conn.targetHandle === handle,
)
) {
return;
}
console.warn("Error", error);
isValid = false;
if (path && error.message) {
const key = path.slice(1);
console.log("Error", key, error.message);
setNestedProperty(
errors,
key,
error.message[0].toUpperCase() + error.message.slice(1),
);
} else if (error.keyword === "required") {
const key = error.params.missingProperty;
setNestedProperty(errors, key, "This field is required");
}
});
}
// Set errors
setNodes((nodes) => {
return nodes.map((n) => {
if (n.id === node.id) {
return {
...n,
data: {
...n.data,
errors,
},
};
}
return n;
});
});
});
return isValid;
}, [nodes]);
// Handle user requests
useEffect(() => {
// Ignore none request
if (saveRunRequest.request === "none") {
return;
}
// Display error message
if (saveRunRequest.state === "error") {
if (saveRunRequest.request === "save") {
console.error("Error saving agent");
} else if (saveRunRequest.request === "run") {
console.error(`Error saving&running agent`);
} else if (saveRunRequest.request === "stop") {
console.error(`Error stopping agent`);
}
// Reset request
setSaveRunRequest({
request: "none",
state: "none",
});
return;
}
// When saving request is done
if (
saveRunRequest.state === "saving" &&
savedAgent &&
nodesSyncedWithSavedAgent
) {
// Reset request if only save was requested
if (saveRunRequest.request === "save") {
setSaveRunRequest({
request: "none",
state: "none",
});
// If run was requested, run the agent
} else if (saveRunRequest.request === "run") {
if (!validateNodes()) {
console.error("Validation failed; aborting run");
setSaveRunRequest({
request: "none",
state: "none",
});
return;
}
api.subscribeToExecution(savedAgent.id);
setSaveRunRequest({ request: "run", state: "running" });
api
.executeGraph(savedAgent.id)
.then((graphExecution) => {
setSaveRunRequest({
request: "run",
state: "running",
activeExecutionID: graphExecution.id,
});
// Track execution until completed
const pendingNodeExecutions: Set<string> = new Set();
const cancelExecListener = api.onWebSocketMessage(
"execution_event",
(nodeResult) => {
// We are racing the server here, since we need the ID to filter events
if (nodeResult.graph_exec_id != graphExecution.id) {
return;
}
if (
nodeResult.status != "COMPLETED" &&
nodeResult.status != "FAILED"
) {
pendingNodeExecutions.add(nodeResult.node_exec_id);
} else {
pendingNodeExecutions.delete(nodeResult.node_exec_id);
}
if (pendingNodeExecutions.size == 0) {
// Assuming the first event is always a QUEUED node, and
// following nodes are QUEUED before all preceding nodes are COMPLETED,
// an empty set means the graph has finished running.
cancelExecListener();
setSaveRunRequest({ request: "none", state: "none" });
}
},
);
})
.catch(() => setSaveRunRequest({ request: "run", state: "error" }));
processedUpdates.current = processedUpdates.current = [];
}
}
// Handle stop request
if (
saveRunRequest.request === "stop" &&
saveRunRequest.state != "stopping" &&
savedAgent &&
saveRunRequest.activeExecutionID
) {
setSaveRunRequest({
request: "stop",
state: "stopping",
activeExecutionID: saveRunRequest.activeExecutionID,
});
api
.stopGraphExecution(savedAgent.id, saveRunRequest.activeExecutionID)
.then(() => setSaveRunRequest({ request: "none", state: "none" }));
}
}, [
api,
saveRunRequest,
savedAgent,
nodesSyncedWithSavedAgent,
validateNodes,
]);
// Check if node ids are synced with saved agent
useEffect(() => {
// Check if all node ids are synced with saved agent (frontend and backend)
if (!savedAgent || nodes?.length === 0) {
setNodesSyncedWithSavedAgent(false);
return;
}
// Find at least one node that has backend id existing on any saved agent node
// This will works as long as ALL ids are replaced each time the graph is run
const oneNodeSynced = savedAgent.nodes.some(
(backendNode) => backendNode.id === nodes[0].data.backend_id,
);
setNodesSyncedWithSavedAgent(oneNodeSynced);
}, [savedAgent, nodes]);
const prepareNodeInputData = useCallback(
(node: CustomNode) => {
console.debug(
"Preparing input data for node:",
node.id,
node.data.blockType,
);
const blockSchema = availableNodes.find(
(n) => n.id === node.data.block_id,
)?.inputSchema;
if (!blockSchema) {
console.error(`Schema not found for block ID: ${node.data.block_id}`);
return {};
}
const getNestedData = (
schema: BlockIOSubSchema,
values: { [key: string]: any },
): { [key: string]: any } => {
let inputData: { [key: string]: any } = {};
if ("properties" in schema) {
Object.keys(schema.properties).forEach((key) => {
if (values[key] !== undefined) {
if (
"properties" in schema.properties[key] ||
"additionalProperties" in schema.properties[key]
) {
inputData[key] = getNestedData(
schema.properties[key],
values[key],
);
} else {
inputData[key] = values[key];
}
}
});
}
if ("additionalProperties" in schema) {
inputData = { ...inputData, ...values };
}
return inputData;
};
let inputData = getNestedData(blockSchema, node.data.hardcodedValues);
console.debug(
`Final prepared input for ${node.data.blockType} (${node.id}):`,
inputData,
);
return inputData;
},
[availableNodes],
);
const saveAgent = useCallback(
async (asTemplate: boolean = false) => {
//FIXME frontend ids should be resolved better (e.g. returned from the server)
// currently this relays on block_id and position
const blockIdToNodeIdMap: Record<string, string> = {};
nodes.forEach((node) => {
const key = `${node.data.block_id}_${node.position.x}_${node.position.y}`;
blockIdToNodeIdMap[key] = node.id;
});
const formattedNodes = nodes.map((node) => {
const inputDefault = prepareNodeInputData(node);
const inputNodes = edges
.filter((edge) => edge.target === node.id)
.map((edge) => ({
name: edge.targetHandle || "",
node_id: edge.source,
}));
const outputNodes = edges
.filter((edge) => edge.source === node.id)
.map((edge) => ({
name: edge.sourceHandle || "",
node_id: edge.target,
}));
return {
id: node.id,
block_id: node.data.block_id,
input_default: inputDefault,
input_nodes: inputNodes,
output_nodes: outputNodes,
data: {
...node.data,
hardcodedValues: removeEmptyStringsAndNulls(
node.data.hardcodedValues,
),
},
metadata: { position: node.position },
};
});
const links = edges.map((edge) => ({
source_id: edge.source,
sink_id: edge.target,
source_name: edge.sourceHandle || "",
sink_name: edge.targetHandle || "",
}));
const payload = {
id: savedAgent?.id!,
name: agentName || "Agent Name",
description: agentDescription || "Agent Description",
nodes: formattedNodes,
links: links,
};
if (savedAgent && deepEquals(payload, savedAgent)) {
console.debug(
"No need to save: Graph is the same as version on server",
);
// Trigger state change
setSavedAgent(savedAgent);
return;
} else {
console.debug(
"Saving new Graph version; old vs new:",
savedAgent,
payload,
);
}
setNodesSyncedWithSavedAgent(false);
const newSavedAgent = savedAgent
? await (savedAgent.is_template
? api.updateTemplate(savedAgent.id, payload)
: api.updateGraph(savedAgent.id, payload))
: await (asTemplate
? api.createTemplate(payload)
: api.createGraph(payload));
console.debug("Response from the API:", newSavedAgent);
// Update the node IDs on the frontend
setSavedAgent(newSavedAgent);
setNodes((prev) => {
return newSavedAgent.nodes
.map((backendNode) => {
const key = `${backendNode.block_id}_${backendNode.metadata.position.x}_${backendNode.metadata.position.y}`;
const frontendNodeId = blockIdToNodeIdMap[key];
const frontendNode = prev.find(
(node) => node.id === frontendNodeId,
);
return frontendNode
? {
...frontendNode,
position: backendNode.metadata.position,
data: {
...frontendNode.data,
hardcodedValues: removeEmptyStringsAndNulls(
frontendNode.data.hardcodedValues,
),
status: undefined,
backend_id: backendNode.id,
executionResults: [],
},
}
: null;
})
.filter((node) => node !== null);
});
// Reset bead count
setEdges((edges) => {
return edges.map((edge) => ({
...edge,
data: {
...edge.data,
edgeColor: edge.data?.edgeColor!,
beadUp: 0,
beadDown: 0,
beadData: [],
},
}));
});
},
[
api,
nodes,
edges,
savedAgent,
agentName,
agentDescription,
prepareNodeInputData,
],
);
const requestSave = useCallback(
(asTemplate: boolean) => {
saveAgent(asTemplate);
setSaveRunRequest({
request: "save",
state: "saving",
});
},
[saveAgent],
);
const requestSaveAndRun = useCallback(() => {
saveAgent();
setSaveRunRequest({
request: "run",
state: "saving",
});
}, [saveAgent]);
const requestStopRun = useCallback(() => {
if (saveRunRequest.state != "running") {
return;
}
if (!saveRunRequest.activeExecutionID) {
console.warn(
"Stop requested but execution ID is unknown; state:",
saveRunRequest,
);
}
setSaveRunRequest((prev) => ({
...prev,
request: "stop",
state: "running",
}));
}, [saveRunRequest]);
return {
agentName,
setAgentName,
agentDescription,
setAgentDescription,
savedAgent,
availableNodes,
getOutputType,
requestSave,
requestSaveAndRun,
requestStopRun,
isSaving: saveRunRequest.state == "saving",
isRunning: saveRunRequest.state == "running",
isStopping: saveRunRequest.state == "stopping",
nodes,
setNodes,
edges,
setEdges,
};
}

View File

@ -0,0 +1,157 @@
import { useCallback, useMemo } from "react";
type XYPosition = {
x: number;
y: number;
};
export type BezierPath = {
sourcePosition: XYPosition;
control1: XYPosition;
control2: XYPosition;
targetPosition: XYPosition;
};
export function useBezierPath(
sourceX: number,
sourceY: number,
targetX: number,
targetY: number,
) {
const path: BezierPath = useMemo(() => {
const xDifference = Math.abs(sourceX - targetX);
const yDifference = Math.abs(sourceY - targetY);
const xControlDistance =
sourceX < targetX ? 64 : Math.max(xDifference / 2, 64);
const yControlDistance = yDifference < 128 && sourceX > targetX ? -64 : 0;
return {
sourcePosition: { x: sourceX, y: sourceY },
control1: {
x: sourceX + xControlDistance,
y: sourceY + yControlDistance,
},
control2: {
x: targetX - xControlDistance,
y: targetY + yControlDistance,
},
targetPosition: { x: targetX, y: targetY },
};
}, [sourceX, sourceY, targetX, targetY]);
const svgPath = useMemo(
() =>
`M ${path.sourcePosition.x} ${path.sourcePosition.y} ` +
`C ${path.control1.x} ${path.control1.y} ${path.control2.x} ${path.control2.y} ` +
`${path.targetPosition.x}, ${path.targetPosition.y}`,
[path],
);
const getPointForT = useCallback(
(t: number) => {
// Bezier formula: (1-t)^3 * p0 + 3*(1-t)^2*t*p1 + 3*(1-t)*t^2*p2 + t^3*p3
const x =
Math.pow(1 - t, 3) * path.sourcePosition.x +
3 * Math.pow(1 - t, 2) * t * path.control1.x +
3 * (1 - t) * Math.pow(t, 2) * path.control2.x +
Math.pow(t, 3) * path.targetPosition.x;
const y =
Math.pow(1 - t, 3) * path.sourcePosition.y +
3 * Math.pow(1 - t, 2) * t * path.control1.y +
3 * (1 - t) * Math.pow(t, 2) * path.control2.y +
Math.pow(t, 3) * path.targetPosition.y;
return { x, y };
},
[path],
);
const getArcLength = useCallback(
(t: number, samples: number = 100) => {
let length = 0;
let prevPoint = getPointForT(0);
for (let i = 1; i <= samples; i++) {
const currT = (i / samples) * t;
const currPoint = getPointForT(currT);
length += Math.sqrt(
Math.pow(currPoint.x - prevPoint.x, 2) +
Math.pow(currPoint.y - prevPoint.y, 2),
);
prevPoint = currPoint;
}
return length;
},
[getPointForT],
);
const length = useMemo(() => {
return getArcLength(1);
}, [getArcLength]);
const getBezierDerivative = useCallback(
(t: number) => {
const mt = 1 - t;
const x =
3 *
(mt * mt * (path.control1.x - path.sourcePosition.x) +
2 * mt * t * (path.control2.x - path.control1.x) +
t * t * (path.targetPosition.x - path.control2.x));
const y =
3 *
(mt * mt * (path.control1.y - path.sourcePosition.y) +
2 * mt * t * (path.control2.y - path.control1.y) +
t * t * (path.targetPosition.y - path.control2.y));
return { x, y };
},
[path],
);
const getTForDistance = useCallback(
(distance: number, epsilon: number = 0.0001) => {
if (distance < 0) {
distance = length + distance; // If distance is negative, calculate from the end of the curve
}
let t = distance / getArcLength(1);
let prevT = 0;
while (Math.abs(t - prevT) > epsilon) {
prevT = t;
const length = getArcLength(t);
const derivative = Math.sqrt(
Math.pow(getBezierDerivative(t).x, 2) +
Math.pow(getBezierDerivative(t).y, 2),
);
t -= (length - distance) / derivative;
t = Math.max(0, Math.min(1, t)); // Clamp t between 0 and 1
}
return t;
},
[getArcLength, getBezierDerivative, length],
);
const getPointAtDistance = useCallback(
(distance: number) => {
if (distance < 0) {
distance = length + distance; // If distance is negative, calculate from the end of the curve
}
const t = getTForDistance(distance);
return getPointForT(t);
},
[getTForDistance, getPointForT, length],
);
return {
path,
svgPath,
length,
getPointForT,
getTForDistance,
getPointAtDistance,
};
}

View File

@ -10,6 +10,7 @@ const useUser = () => {
const [session, setSession] = useState<Session | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [role, setRole] = useState<string | null>(null);
useEffect(() => {
if (isSupabaseLoading || !supabase) {
@ -19,16 +20,21 @@ const useUser = () => {
const fetchUser = async () => {
try {
setIsLoading(true);
const {
data: { user },
} = await supabase.auth.getUser();
const {
data: { session },
} = await supabase.auth.getSession();
setUser(user);
setSession(session);
const { data: userData, error: userError } =
await supabase.auth.getUser();
const { data: sessionData, error: sessionError } =
await supabase.auth.getSession();
if (userError) throw new Error(`User error: ${userError.message}`);
if (sessionError)
throw new Error(`Session error: ${sessionError.message}`);
setUser(userData.user);
setSession(sessionData.session);
setRole(userData.user?.role || null);
} catch (e) {
setError("Failed to fetch user data");
setError(e instanceof Error ? e.message : "Failed to fetch user data");
console.error("Error in useUser hook:", e);
} finally {
setIsLoading(false);
}
@ -41,13 +47,21 @@ const useUser = () => {
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
setUser(session?.user ?? null);
setRole(session?.user?.role || null);
setIsLoading(false);
});
return () => subscription.unsubscribe();
}, [supabase, isSupabaseLoading]);
return { user, session, isLoading: isLoading || isSupabaseLoading, error };
return {
user,
session,
role,
isLoading: isLoading || isSupabaseLoading,
error,
};
};
export default useUser;

View File

@ -7,6 +7,7 @@ import {
GraphMeta,
GraphExecuteResponse,
NodeExecutionResult,
User,
} from "./types";
export default class AutoGPTServerAPI {
@ -14,15 +15,21 @@ export default class AutoGPTServerAPI {
private wsUrl: string;
private webSocket: WebSocket | null = null;
private wsConnecting: Promise<void> | null = null;
private wsMessageHandlers: { [key: string]: (data: any) => void } = {};
private wsMessageHandlers: Record<string, Set<(data: any) => void>> = {};
private supabaseClient = createClient();
constructor(
baseUrl: string = process.env.NEXT_PUBLIC_AGPT_SERVER_URL ||
"http://localhost:8000/api",
wsUrl: string = process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL ||
"ws://localhost:8001/ws",
) {
this.baseUrl = baseUrl;
this.wsUrl = `ws://${new URL(this.baseUrl).host}/ws`;
this.wsUrl = wsUrl;
}
async createUser(): Promise<User> {
return this._request("POST", "/auth/user", {});
}
async getBlocks(): Promise<Block[]> {
@ -121,16 +128,19 @@ export default class AutoGPTServerAPI {
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,
}),
parseNodeExecutionResultTimestamps,
);
}
async stopGraphExecution(
graphID: string,
runID: string,
): Promise<NodeExecutionResult[]> {
return (
await this._request("POST", `/graphs/${graphID}/executions/${runID}/stop`)
).map(parseNodeExecutionResultTimestamps);
}
private async _get(path: string) {
return this._request("GET", path);
}
@ -185,12 +195,12 @@ export default class AutoGPTServerAPI {
this.webSocket = new WebSocket(wsUrlWithToken);
this.webSocket.onopen = () => {
console.log("WebSocket connection established");
console.debug("WebSocket connection established");
resolve();
};
this.webSocket.onclose = (event) => {
console.log("WebSocket connection closed", event);
console.debug("WebSocket connection closed", event);
this.webSocket = null;
};
@ -200,10 +210,13 @@ export default class AutoGPTServerAPI {
};
this.webSocket.onmessage = (event) => {
const message = JSON.parse(event.data);
if (this.wsMessageHandlers[message.method]) {
this.wsMessageHandlers[message.method](message.data);
const message: WebsocketMessage = JSON.parse(event.data);
if (message.method == "execution_event") {
message.data = parseNodeExecutionResultTimestamps(message.data);
}
this.wsMessageHandlers[message.method]?.forEach((handler) =>
handler(message.data),
);
};
} catch (error) {
console.error("Error connecting to WebSocket:", error);
@ -222,33 +235,38 @@ export default class AutoGPTServerAPI {
sendWebSocketMessage<M extends keyof WebsocketMessageTypeMap>(
method: M,
data: WebsocketMessageTypeMap[M],
callCount = 0,
) {
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
this.webSocket.send(JSON.stringify({ method, data }));
} else {
this.connectWebSocket().then(() =>
this.sendWebSocketMessage(method, data),
);
this.connectWebSocket().then(() => {
callCount == 0
? this.sendWebSocketMessage(method, data, callCount + 1)
: setTimeout(
() => {
this.sendWebSocketMessage(method, data, callCount + 1);
},
2 ** (callCount - 1) * 1000,
);
});
}
}
onWebSocketMessage<M extends keyof WebsocketMessageTypeMap>(
method: M,
handler: (data: WebsocketMessageTypeMap[M]) => void,
) {
this.wsMessageHandlers[method] = handler;
): () => void {
this.wsMessageHandlers[method] ??= new Set();
this.wsMessageHandlers[method].add(handler);
// Return detacher
return () => this.wsMessageHandlers[method].delete(handler);
}
subscribeToExecution(graphId: string) {
this.sendWebSocketMessage("subscribe", { graph_id: graphId });
}
runGraph(
graphId: string,
data: WebsocketMessageTypeMap["run_graph"]["data"] = {},
) {
this.sendWebSocketMessage("run_graph", { graph_id: graphId, data });
}
}
/* *** UTILITY TYPES *** */
@ -264,6 +282,24 @@ type GraphCreateRequestBody =
type WebsocketMessageTypeMap = {
subscribe: { graph_id: string };
run_graph: { graph_id: string; data: { [key: string]: any } };
execution_event: NodeExecutionResult;
};
type WebsocketMessage = {
[M in keyof WebsocketMessageTypeMap]: {
method: M;
data: WebsocketMessageTypeMap[M];
};
}[keyof WebsocketMessageTypeMap];
/* *** HELPER FUNCTIONS *** */
function parseNodeExecutionResultTimestamps(result: any): NodeExecutionResult {
return {
...result,
add_time: new Date(result.add_time),
queue_time: result.queue_time ? new Date(result.queue_time) : undefined,
start_time: result.start_time ? new Date(result.start_time) : undefined,
end_time: result.end_time ? new Date(result.end_time) : undefined,
};
}

View File

@ -1,16 +1,25 @@
/* Mirror of autogpt_server/data/block.py:Block */
export type Category = {
category: string;
description: string;
};
export type Block = {
id: string;
name: string;
description: string;
categories: Category[];
inputSchema: BlockIORootSchema;
outputSchema: BlockIORootSchema;
staticOutput: boolean;
uiType: BlockUIType;
};
export type BlockIORootSchema = {
type: "object";
properties: { [key: string]: BlockIOSubSchema };
required?: string[];
required?: (keyof BlockIORootSchema["properties"])[];
additionalProperties?: { type: string };
};
@ -27,17 +36,18 @@ type BlockIOSimpleTypeSubSchema =
| BlockIOBooleanSubSchema
| BlockIONullSubSchema;
type BlockIOSubSchemaMeta = {
export type BlockIOSubSchemaMeta = {
title?: string;
description?: string;
placeholder?: string;
advanced?: boolean;
};
export type BlockIOObjectSubSchema = BlockIOSubSchemaMeta & {
type: "object";
properties: { [key: string]: BlockIOSubSchema };
default?: { [key: keyof BlockIOObjectSubSchema["properties"]]: any };
required?: keyof BlockIOObjectSubSchema["properties"][];
required?: (keyof BlockIOObjectSubSchema["properties"])[];
};
export type BlockIOKVSubSchema = BlockIOSubSchemaMeta & {
@ -110,9 +120,10 @@ export type Link = {
sink_id: string;
source_name: string;
sink_name: string;
is_static: boolean;
};
export type LinkCreatable = Omit<Link, "id"> & {
export type LinkCreatable = Omit<Link, "id" | "is_static"> & {
id?: string;
};
@ -167,3 +178,15 @@ export type NodeExecutionResult = {
start_time?: Date;
end_time?: Date;
};
export type User = {
id: string;
email: string;
};
export enum BlockUIType {
STANDARD = "Standard",
INPUT = "Input",
OUTPUT = "Output",
NOTE = "Note",
}

View File

@ -6,6 +6,9 @@ import {
AgentListResponse,
AgentDetailResponse,
AgentWithRank,
FeaturedAgentResponse,
UniqueCategoriesResponse,
AnalyticsEvent,
} from "./types";
export default class MarketplaceAPI {
@ -99,6 +102,20 @@ export default class MarketplaceAPI {
return this._get(`/agents/${id}/download?${queryParams.toString()}`);
}
async submitAgent(
graph: { [key: string]: any },
author: string,
keywords: string[],
categories: string[],
): Promise<AgentResponse> {
return this._post("/agents/submit", {
graph,
author,
keywords,
categories,
});
}
async downloadAgentFile(id: string, version?: number): Promise<Blob> {
const queryParams = new URLSearchParams();
if (version) queryParams.append("version", version.toString());
@ -107,10 +124,83 @@ export default class MarketplaceAPI {
);
}
async getAgentSubmissions(): Promise<AgentListResponse> {
return this._get("/admin/agent/submissions");
}
async createAgentEntry(request: AddAgentRequest): Promise<AgentResponse> {
return this._post("/admin/agent", request);
}
async approveAgentSubmission(
agentId: string,
version: number,
comments: string = "",
): Promise<AgentResponse> {
return this._post("/admin/agent/submissions", {
agent_id: agentId,
version: version,
status: "APPROVED",
comments: comments,
});
}
async rejectAgentSubmission(
agentId: string,
version: number,
comments: string = "",
): Promise<AgentResponse> {
return this._post("/admin/agent/submissions", {
agent_id: agentId,
version: version,
status: "REJECTED",
comments: comments,
});
}
async addFeaturedAgent(
agentId: string,
categories: string[],
): Promise<FeaturedAgentResponse> {
const response = await this._post(`/admin/agent/featured/${agentId}`, {
categories: categories,
});
return response;
}
async removeFeaturedAgent(
agentId: string,
categories: string[],
): Promise<FeaturedAgentResponse> {
return this._delete(`/admin/agent/featured/${agentId}`, {
categories: categories,
});
}
async getFeaturedAgent(agentId: string): Promise<FeaturedAgentResponse> {
return this._get(`/admin/agent/featured/${agentId}`);
}
async getNotFeaturedAgents(
page: number = 1,
pageSize: number = 10,
): Promise<AgentListResponse> {
return this._get(
`/admin/agent/not-featured?page=${page}&page_size=${pageSize}`,
);
}
async getCategories(): Promise<UniqueCategoriesResponse> {
return this._get("/admin/categories");
}
async makeAnalyticsEvent(event: AnalyticsEvent) {
if (event.event_name === "agent_installed_from_marketplace") {
return this._post("/analytics/agent-installed", event.event_data);
}
throw new Error("Invalid event name");
}
private async _get(path: string) {
return this._request("GET", path);
}
@ -119,6 +209,10 @@ export default class MarketplaceAPI {
return this._request("POST", path, payload);
}
private async _delete(path: string, payload: { [key: string]: any }) {
return this._request("DELETE", path, payload);
}
private async _getBlob(path: string): Promise<Blob> {
const response = await fetch(this.baseUrl + path);
if (!response.ok) {
@ -134,7 +228,7 @@ export default class MarketplaceAPI {
}
private async _request(
method: "GET" | "POST" | "PUT" | "PATCH",
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
path: string,
payload?: { [key: string]: any },
) {

View File

@ -43,6 +43,22 @@ export type AgentList = {
total_pages: number;
};
export type FeaturedAgentResponse = {
agentId: string;
featuredCategories: string[];
createdAt: string; // ISO8601 datetime string
updatedAt: string; // ISO8601 datetime string
isActive: boolean;
};
export type FeaturedAgentsList = {
agents: FeaturedAgentResponse[];
total_count: number;
page: number;
page_size: number;
total_pages: number;
};
export type AgentDetail = Agent & {
graph: Record<string, any>;
};
@ -56,3 +72,38 @@ export type AgentListResponse = AgentList;
export type AgentDetailResponse = AgentDetail;
export type AgentResponse = Agent;
export type UniqueCategoriesResponse = {
unique_categories: string[];
};
export enum InstallationLocation {
LOCAL = "local",
CLOUD = "cloud",
}
export type AgentInstalledFromMarketplaceEventData = {
marketplace_agent_id: string;
installed_agent_id: string;
installation_location: InstallationLocation;
};
export type AgentInstalledFromTemplateEventData = {
template_id: string;
installed_agent_id: string;
installation_location: InstallationLocation;
};
export interface AgentInstalledFromMarketplaceEvent {
event_name: "agent_installed_from_marketplace";
event_data: AgentInstalledFromMarketplaceEventData;
}
export interface AgentInstalledFromTemplateEvent {
event_name: "agent_installed_from_template";
event_data: AgentInstalledFromTemplateEventData;
}
export type AnalyticsEvent =
| AgentInstalledFromMarketplaceEvent
| AgentInstalledFromTemplateEvent;

View File

@ -45,6 +45,7 @@ export async function updateSession(request: NextRequest) {
const {
data: { user },
error,
} = await supabase.auth.getUser();
if (

View File

@ -1,5 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { Category } from "./autogpt-server-api/types";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
@ -36,7 +37,7 @@ export function deepEquals(x: any, y: any): boolean {
/** Get tailwind text color class from type name */
export function getTypeTextColor(type: string | null): string {
if (type === null) return "bg-gray-500";
if (type === null) return "text-gray-500";
return (
{
string: "text-green-500",
@ -53,23 +54,23 @@ export function getTypeTextColor(type: string | null): string {
/** Get tailwind bg color class from type name */
export function getTypeBgColor(type: string | null): string {
if (type === null) return "bg-gray-500";
if (type === null) return "border-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",
any: "bg-gray-500",
"": "bg-gray-500",
}[type] || "bg-gray-500"
string: "border-green-500",
number: "border-blue-500",
boolean: "border-yellow-500",
object: "border-purple-500",
array: "border-indigo-500",
null: "border-gray-500",
any: "border-gray-500",
"": "border-gray-500",
}[type] || "border-gray-500"
);
}
export function getTypeColor(type: string | null): string {
if (type === null) return "bg-gray-500";
if (type === null) return "#6b7280";
return (
{
string: "#22c55e",
@ -175,3 +176,21 @@ export function removeEmptyStringsAndNulls(obj: any): any {
}
return obj;
}
export const categoryColorMap: Record<string, string> = {
AI: "bg-orange-300/[.7]",
SOCIAL: "bg-yellow-300/[.7]",
TEXT: "bg-green-300/[.7]",
SEARCH: "bg-blue-300/[.7]",
BASIC: "bg-purple-300/[.7]",
INPUT: "bg-cyan-300/[.7]",
OUTPUT: "bg-brown-300/[.7]",
LOGIC: "bg-teal-300/[.7]",
};
export function getPrimaryCategoryColor(categories: Category[]): string {
if (categories.length === 0) {
return "bg-gray-300/[.7]";
}
return categoryColorMap[categories[0].category] || "bg-gray-300/[.7]";
}

View File

@ -0,0 +1,16 @@
import { redirect } from "next/navigation";
import getServerUser from "@/hooks/getServerUser";
import React from "react";
export async function withRoleAccess(allowedRoles: string[]) {
"use server";
return async function <T extends React.ComponentType<any>>(Component: T) {
const { user, role, error } = await getServerUser();
if (error || !user || !role || !allowedRoles.includes(role)) {
redirect("/unauthorized");
}
return Component;
};
}

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ security = HTTPBearer()
async def auth_middleware(request: Request):
if not settings.ENABLE_AUTH:
# If authentication is disabled, allow the request to proceed
logging.warn("Auth disabled")
return {}
security = HTTPBearer()

View File

@ -0,0 +1,8 @@
from .store import SupabaseIntegrationCredentialsStore
from .types import APIKeyCredentials, OAuth2Credentials
__all__ = [
"SupabaseIntegrationCredentialsStore",
"APIKeyCredentials",
"OAuth2Credentials",
]

Some files were not shown because too many files have changed in this diff Show More