Compare commits

..

10 Commits

Author SHA1 Message Date
SwiftyOS
f713c75464 update meme generator graph 2024-08-19 12:18:52 +02:00
SwiftyOS
a9b8ce13e9 Added example graph 2024-08-16 10:13:50 +02:00
SwiftyOS
c2cd5a4664 Add blocks 2024-08-16 10:12:42 +02:00
Reinier van der Leer
66dfeedcb1 terminate RSS block without delay if not running continuously 2024-08-14 11:45:54 +02:00
Reinier van der Leer
62107c0dd2 fix You Tube block spelling 2024-08-14 11:37:36 +02:00
Reinier van der Leer
ed3be26365 feat(server): Add Dall-E block 2024-08-14 11:37:20 +02:00
SwiftyOS
8d241acd6d added categories 2024-08-14 11:34:10 +02:00
SwiftyOS
8643f70cf2 Added returning as base64 string 2024-08-14 11:24:40 +02:00
SwiftyOS
42af69648d tts v1 working 2024-08-14 11:22:33 +02:00
SwiftyOS
b5b47146c6 added empty files 2024-08-14 11:09:47 +02:00
304 changed files with 8372 additions and 26120 deletions

View File

@@ -34,7 +34,5 @@ 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,10 +31,12 @@ 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' }}
@@ -114,28 +116,31 @@ jobs:
- name: Install Python dependencies
run: poetry install
- name: Generate Prisma Client
run: poetry run prisma generate
- name: Generate Prisma Client (Postgres)
if: matrix.db-platform == 'postgres'
run: poetry run prisma generate --schema postgres/schema.prisma
- name: Run Database Migrations
run: poetry run prisma migrate dev --name updates
- name: Run Database Migrations (Postgres)
if: matrix.db-platform == 'postgres'
run: poetry run prisma migrate dev --schema postgres/schema.prisma --name updates
env:
CONNECTION_STR: ${{ steps.postgres.outputs.connection-uri }}
- id: lint
name: Run Linter
- 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
run: poetry run lint
- name: Run pytest with coverage
run: |
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' }}
poetry run pytest -vv \
test
env:
CI: true
PLAIN_OUTPUT: True

View File

@@ -0,0 +1,55 @@
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,31 +1,51 @@
name: PR Status Checker
on:
pull_request:
types: [opened, synchronize, reopened]
workflow_run:
workflows: ["*"]
types:
- completed
jobs:
status-check:
name: Check PR Status
name: Check Actions Status
runs-on: ubuntu-latest
steps:
# - 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 }}
- 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 }}

View File

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

View File

@@ -1,109 +0,0 @@
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,8 +69,6 @@ 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

@@ -1,17 +0,0 @@
# 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,16 +56,6 @@ 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
@@ -73,7 +63,6 @@ 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
```
@@ -88,12 +77,11 @@ 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 commands, in the same terminal you ran the `poetry install` command:
In order to setup the database, you need to run the following command, in the same terminal you ran the `poetry install` command:
```sh
docker compose up postgres redis -d
poetry run prisma migrate dev
```
```bash
poetry run prisma migrate deploy
```
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
@@ -102,24 +90,15 @@ poetry run prisma generate
Without running this command, the necessary Python modules (prisma.models) won't be available, leading to a `ModuleNotFoundError`.
### Running the server without Docker
### Running the server
To run the server, you can run the following commands in the same terminal you ran the `poetry install` command:
To run the server, you can run the following command in the same terminal you ran the `poetry install` command:
```bash
poetry run app
```
### 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:
In the other terminal, you can run the following command to start the frontend:
```bash
yarn dev
@@ -128,10 +107,3 @@ 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,7 +10,6 @@ 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,114 +1,36 @@
# AutoGPT Platform
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.
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.
https://github.com/user-attachments/assets/fd0d0f35-3155-4263-b575-ba3efb126cb4
## Getting Started
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.
### 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.
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.
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,6 +1,5 @@
NEXT_PUBLIC_AGPT_SERVER_URL=http://localhost:8000/api
NEXT_PUBLIC_AGPT_WS_SERVER_URL=ws://localhost:8001/ws
NEXT_PUBLIC_AGPT_MARKETPLACE_URL=http://localhost:8005/api/v1/market
NEXT_PUBLIC_AGPT_MARKETPLACE_URL=http://localhost:8001/api/v1/market
## Supabase credentials
## YOU ONLY NEED THEM IF YOU WANT TO USE SUPABASE USER AUTHENTICATION
@@ -12,4 +11,3 @@ NEXT_PUBLIC_AGPT_MARKETPLACE_URL=http://localhost:8005/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

@@ -1,3 +0,0 @@
{
"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 rnd/autogpt_builder/package.json rnd/autogpt_builder/yarn.lock ./
COPY autogpt_builder/package.json autogpt_builder/yarn.lock ./
RUN yarn install --frozen-lockfile
# Dev stage
FROM base AS dev
ENV NODE_ENV=development
COPY rnd/autogpt_builder/ .
COPY autogpt_builder/ .
EXPOSE 3000
CMD ["yarn", "run", "dev"]
CMD ["npm", "run", "dev"]
# Build stage for prod
FROM base AS build
COPY rnd/autogpt_builder/ .
COPY autogpt_builder/ .
RUN npm run build
# Prod stage

View File

@@ -10,9 +10,6 @@ const nextConfig = {
NEXT_PUBLIC_AGPT_MARKETPLACE_URL:
process.env.NEXT_PUBLIC_AGPT_MARKETPLACE_URL,
},
images: {
domains: ["images.unsplash.com"],
},
async redirects() {
return [
{

View File

@@ -11,30 +11,24 @@
},
"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-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-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",
@@ -48,11 +42,10 @@
"react-icons": "^5.2.1",
"react-markdown": "^9.0.1",
"react-modal": "^3.16.1",
"react-shepherd": "^6.1.1",
"reactflow": "^11.11.4",
"recharts": "^2.12.7",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^10.0.0",
"zod": "^3.23.8"
},
"devDependencies": {
@@ -64,7 +57,6 @@
"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

@@ -1,18 +0,0 @@
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

@@ -1,100 +0,0 @@
"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

@@ -1,25 +0,0 @@
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

@@ -1,18 +0,0 @@
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

@@ -1,18 +0,0 @@
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

@@ -1,43 +0,0 @@
"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

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

View File

@@ -7,8 +7,6 @@ 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"] });
@@ -32,18 +30,13 @@ export default function RootLayout({
// enableSystem
disableTransitionOnChange
>
<div className="flex min-h-screen flex-col">
<div className="flex flex-col min-h-screen ">
<NavBar />
<main className="flex-1 overflow-hidden p-4">{children}</main>
<main className="flex-1 p-4 overflow-hidden">{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

@@ -1,21 +0,0 @@
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,15 +20,10 @@ 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() {
@@ -43,7 +38,6 @@ export default function LoginPage() {
defaultValues: {
email: "",
password: "",
agreeToTerms: false,
},
});
@@ -54,7 +48,7 @@ export default function LoginPage() {
if (isUserLoading || isSupabaseLoading || user) {
return (
<div className="flex h-[80vh] items-center justify-center">
<div className="flex justify-center items-center h-[80vh]">
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
</div>
);
@@ -77,6 +71,11 @@ 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',
// },
},
});
@@ -112,8 +111,8 @@ export default function LoginPage() {
};
return (
<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="flex items-center justify-center h-[80vh]">
<div className="w-full max-w-md p-8 rounded-lg shadow-md space-y-6">
<div className="mb-6 space-y-2">
<Button
className="w-full"
@@ -177,46 +176,16 @@ export default function LoginPage() {
</FormItem>
)}
/>
<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">
<div className="flex w-full space-x-4 mt-6 mb-6">
<Button
className="flex w-1/2 justify-center"
className="w-1/2 flex justify-center"
type="submit"
disabled={isLoading}
>
Log in
</Button>
<Button
className="flex w-1/2 justify-center"
className="w-1/2 flex justify-center"
variant="outline"
type="button"
onClick={form.handleSubmit(onSignup)}
@@ -226,7 +195,10 @@ export default function LoginPage() {
</Button>
</div>
</form>
<p className="text-sm text-red-500">{feedback}</p>
<p className="text-red-500 text-sm">{feedback}</p>
<p className="text-primary text-center text-sm">
By continuing you agree to everything
</p>
</Form>
</div>
</div>

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/marketplace/AgentDetailContent";
import AgentDetailContent from "@/components/AgentDetailContent";
async function getAgentDetails(id: string): Promise<AgentDetailResponse> {
const apiUrl =

View File

@@ -1,7 +1,6 @@
"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, {
@@ -9,13 +8,7 @@ import MarketplaceAPI, {
AgentListResponse,
AgentWithRank,
} from "@/lib/marketplace-api";
import {
ChevronLeft,
ChevronRight,
PlusCircle,
Search,
Star,
} from "lucide-react";
import { ChevronLeft, ChevronRight, Search, Star } from "lucide-react";
// Utility Functions
function debounce<T extends (...args: any[]) => any>(
@@ -33,61 +26,44 @@ function debounce<T extends (...args: any[]) => any>(
type Agent = AgentResponse | AgentWithRank;
// Components
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>
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>
</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="relative mb-8">
<div className="mb-8 relative">
<Input
placeholder="Search agents..."
type="text"
className="w-full rounded-full border-gray-300 py-2 pl-10 pr-4 focus:border-indigo-500 focus:ring-indigo-500"
className="w-full pl-10 pr-4 py-2 rounded-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
value={value}
onChange={onChange}
/>
<Search
className="absolute left-3 top-1/2 -translate-y-1/2 transform text-gray-400"
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
size={20}
/>
</div>
@@ -105,24 +81,24 @@ const AgentCard: React.FC<{ agent: Agent; featured?: boolean }> = ({
return (
<div
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"}`}
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"}`}
onClick={handleClick}
>
<div>
<div className="mb-2 flex items-center justify-between">
<h3 className="truncate text-lg font-semibold text-gray-900">
<div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-semibold text-gray-900 truncate">
{agent.name}
</h3>
{featured && <Star className="text-indigo-500" size={20} />}
</div>
<p className="mb-4 line-clamp-2 text-sm text-gray-500">
<p className="text-sm text-gray-500 line-clamp-2 mb-4">
{agent.description}
</p>
<div className="mb-2 text-xs text-gray-400">
<div className="text-xs text-gray-400 mb-2">
Categories: {agent.categories.join(", ")}
</div>
</div>
<div className="flex items-end justify-between">
<div className="flex justify-between items-end">
<div className="text-xs text-gray-400">
Updated {new Date(agent.updatedAt).toLocaleDateString()}
</div>
@@ -143,8 +119,8 @@ const AgentGrid: React.FC<{
featured?: boolean;
}> = ({ agents, title, featured = false }) => (
<div className="mb-12">
<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">
<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">
{agents.map((agent) => (
<AgentCard agent={agent} key={agent.id} featured={featured} />
))}
@@ -158,11 +134,11 @@ const Pagination: React.FC<{
onPrevPage: () => void;
onNextPage: () => void;
}> = ({ page, totalPages, onPrevPage, onNextPage }) => (
<div className="mt-8 flex items-center justify-between">
<div className="flex justify-between items-center mt-8">
<Button
onClick={onPrevPage}
disabled={page === 1}
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"
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"
>
<ChevronLeft size={16} />
<span>Previous</span>
@@ -173,7 +149,7 @@ const Pagination: React.FC<{
<Button
onClick={onNextPage}
disabled={page === totalPages}
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"
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"
>
<span>Next</span>
<ChevronRight size={16} />
@@ -272,20 +248,20 @@ const Marketplace: React.FC = () => {
};
return (
<div className="min-h-screen bg-gray-50">
<div className="bg-gray-50 min-h-screen">
<HeroSection />
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<SearchInput value={searchValue} onChange={handleInputChange} />
{isLoading ? (
<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>
<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>
<p className="mt-2 text-gray-600">Loading agents...</p>
</div>
) : searchValue ? (
searchResults.length > 0 ? (
<AgentGrid agents={searchResults} title="Search Results" />
) : (
<div className="py-12 text-center">
<div className="text-center py-12">
<p className="text-gray-600">
No agents found matching your search criteria.
</p>

View File

@@ -1,408 +0,0 @@
"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, { useCallback, useEffect, useMemo, useState } from "react";
import React, { useEffect, useState } from "react";
import AutoGPTServerAPI, {
GraphMeta,
@@ -22,64 +22,61 @@ const Monitor = () => {
const [selectedFlow, setSelectedFlow] = useState<GraphMeta | null>(null);
const [selectedRun, setSelectedRun] = useState<FlowRun | null>(null);
const api = useMemo(() => new AutoGPTServerAPI(), []);
const api = new AutoGPTServerAPI();
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(() => fetchFlowsAndRuns(), []);
useEffect(() => {
const intervalId = setInterval(
() => flows.map((f) => refreshFlowRuns(f.id)),
5000,
);
return () => clearInterval(intervalId);
}, [flows, refreshFlowRuns]);
}, []);
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];
}),
);
}),
);
}
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 gap-4 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10">
<div className="grid grid-cols-1 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10 gap-4">
<AgentFlowList
className={column1}
flows={flows}
@@ -93,11 +90,10 @@ const Monitor = () => {
<FlowRunsList
className={column2}
flows={flows}
runs={[
...(selectedFlow
? flowRuns.filter((v) => v.graphID == selectedFlow.id)
: flowRuns),
].sort((a, b) => Number(a.startTime) - Number(b.startTime))}
runs={(selectedFlow
? flowRuns.filter((v) => v.graphID == selectedFlow.id)
: flowRuns
).toSorted((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 h-[80vh] items-center justify-center">
<div className="flex justify-center items-center h-[80vh]">
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
</div>
);

View File

@@ -1,9 +0,0 @@
// 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

@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useMemo, useState } from "react";
import Link from "next/link";
import {
ArrowLeft,
@@ -11,31 +11,30 @@ import {
ChevronUp,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
AgentDetailResponse,
InstallationLocation,
} from "@/lib/marketplace-api";
import { AgentDetailResponse } from "@/lib/marketplace-api";
import dynamic from "next/dynamic";
import { Node, Edge } from "@xyflow/react";
import { Node, Edge, NodeTypes, EdgeTypes } from "reactflow";
import MarketplaceAPI from "@/lib/marketplace-api";
import AutoGPTServerAPI, { GraphCreatable } from "@/lib/autogpt-server-api";
const ReactFlow = dynamic(
() => import("@xyflow/react").then((mod) => mod.ReactFlow),
() => import("reactflow").then((mod) => mod.default),
{ ssr: false },
);
const Controls = dynamic(
() => import("@xyflow/react").then((mod) => mod.Controls),
() => import("reactflow").then((mod) => mod.Controls),
{ ssr: false },
);
const Background = dynamic(
() => import("@xyflow/react").then((mod) => mod.Background),
() => import("reactflow").then((mod) => mod.Background),
{ ssr: false },
);
import "@xyflow/react/dist/style.css";
import "reactflow/dist/style.css";
import CustomNode from "./CustomNode";
import { CustomEdge } from "./CustomEdge";
import ConnectionLine from "./ConnectionLine";
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) => {
@@ -100,16 +99,8 @@ async function installGraph(id: string): Promise<void> {
nodes: agent.graph.nodes,
links: agent.graph.links,
};
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);
await serverAPI.createTemplate(data);
console.log(`Agent installed successfully`);
} catch (error) {
console.error(`Error installing agent:`, error);
throw error;
@@ -120,9 +111,12 @@ 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="mx-auto max-w-7xl px-4 py-4 sm:px-6 lg:px-8">
<div className="mb-4 flex items-center justify-between">
<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">
<Link
href="/marketplace"
className="inline-flex items-center text-indigo-600 hover:text-indigo-500"
@@ -132,13 +126,13 @@ function AgentDetailContent({ agent }: { agent: AgentDetailResponse }) {
</Link>
<Button
onClick={() => installGraph(agent.id)}
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"
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"
>
<Download className="mr-2" size={16} />
Download Agent
</Button>
</div>
<div className="overflow-hidden bg-white shadow sm:rounded-lg">
<div className="bg-white shadow overflow-hidden 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">
@@ -147,26 +141,60 @@ 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: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">
<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">
<Calendar className="mr-2" size={16} />
Last Updated
</dt>
<dd className="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{new Date(agent.updatedAt).toLocaleDateString()}
</dd>
</div>
<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">
<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">
<Tag className="mr-2" size={16} />
Categories
</dt>
<dd className="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{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

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

View File

@@ -1,197 +1,89 @@
import React, { useCallback, useContext, useEffect, useState } from "react";
import React, { FC, memo, useMemo, useState } from "react";
import {
BaseEdge,
EdgeLabelRenderer,
EdgeProps,
getBezierPath,
useReactFlow,
XYPosition,
Edge,
Node,
} from "@xyflow/react";
} from "reactflow";
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[];
};
type Bead = {
t: number;
targetT: number;
startTime: number;
};
export type CustomEdge = Edge<CustomEdgeData, "custom">;
export function CustomEdge({
const CustomEdgeFC: FC<EdgeProps<CustomEdgeData>> = ({
id,
data,
selected,
source,
sourcePosition,
sourceX,
sourceY,
target,
targetPosition,
targetX,
targetY,
markerEnd,
}: EdgeProps<CustomEdge>) {
}) => {
const [isHovered, setIsHovered] = useState(false);
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 { deleteElements } = useReactFlow<any, CustomEdgeData>();
const onEdgeRemoveClick = () => {
deleteElements({ edges: [{ id }] });
};
const animationDuration = 500; // Duration in milliseconds for bead to travel the curve
const beadDiameter = 12;
const deltaTime = 16;
const [path, labelX, labelY] = getBezierPath({
sourceX: sourceX - 5,
sourceY,
sourcePosition,
targetX: targetX + 4,
targetY,
targetPosition,
});
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],
// Calculate y difference between source and source node, to adjust self-loop edge
const yDifference = useMemo(
() => sourceY - (data?.sourcePos?.y || 0),
[data?.sourcePos?.y],
);
useEffect(() => {
if (data?.beadUp === 0 && data?.beadDown === 0) {
setBeads({ beads: [], created: 0, destroyed: 0 });
return;
}
// Define special edge path for self-loop
const edgePath =
source === target
? `M ${sourceX - 5} ${sourceY} C ${sourceX + 128} ${sourceY - yDifference - 128} ${targetX - 128} ${sourceY - yDifference - 128} ${targetX + 3}, ${targetY}`
: path;
const 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);
console.table({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
path,
labelX,
labelY,
});
return (
<>
<BaseEdge
path={svgPath}
path={edgePath}
markerEnd={markerEnd}
style={{
strokeWidth: (isHovered ? 3 : 2) + (data?.isStatic ? 0.5 : 0),
strokeWidth: isHovered ? 3 : 2,
stroke:
(data?.edgeColor ?? "#555555") +
(selected || isHovered ? "" : "80"),
strokeDasharray: data?.isStatic ? "5 3" : "0",
}}
/>
<path
d={svgPath}
d={edgePath}
fill="none"
strokeOpacity={0}
strokeWidth={20}
@@ -203,7 +95,7 @@ export function CustomEdge({
<div
style={{
position: "absolute",
transform: `translate(-50%, -50%) translate(${middle.x}px,${middle.y}px)`,
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
pointerEvents: "all",
}}
className="edge-label-renderer"
@@ -218,18 +110,8 @@ export function CustomEdge({
</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,121 +1,91 @@
import React, {
useState,
useEffect,
FC,
memo,
useCallback,
useRef,
useContext,
} from "react";
import { NodeProps, useReactFlow, Node, Edge } from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import { NodeProps, useReactFlow } from "reactflow";
import "reactflow/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, cn, setNestedProperty } from "@/lib/utils";
import { beautifyString, 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 {
NodeGenericInputField,
NodeTextBoxInput,
} from "./node-input-components";
import { CustomEdgeData } from "./CustomEdge";
import { NodeGenericInputField } 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[];
categories: string[];
inputSchema: BlockIORootSchema;
outputSchema: BlockIORootSchema;
hardcodedValues: { [key: string]: any };
connections: ConnectionData;
setHardcodedValues: (values: { [key: string]: any }) => void;
connections: Array<{
edge_id: string;
source: string;
sourceHandle: string;
target: string;
targetHandle: string;
}>;
isOutputOpen: boolean;
status?: NodeExecutionResult["status"];
/** executionResults contains outputs across multiple executions
* with the last element being the most recent output */
executionResults?: {
execId: string;
data: NodeExecutionResult["output_data"];
}[];
output_data?: NodeExecutionResult["output_data"];
block_id: string;
backend_id?: string;
errors?: { [key: string]: string };
isOutputStatic?: boolean;
uiType: BlockUIType;
errors?: { [key: string]: string | null };
setErrors: (errors: { [key: string]: string | null }) => void;
setIsAnyModalOpen?: (isOpen: boolean) => void;
};
export type CustomNode = Node<CustomNodeData, "custom">;
export function CustomNode({ data, id, width, height }: NodeProps<CustomNode>) {
const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
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 [inputModalValue, setInputModalValue] = useState<string>("");
const [modalValue, setModalValue] = useState<string>("");
const [isOutputModalOpen, setIsOutputModalOpen] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const { updateNodeData, deleteElements, addNodes, getNode } = useReactFlow<
CustomNode,
Edge
>();
const { deleteElements } = useReactFlow();
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.executionResults || data.status) {
if (data.output_data || data.status) {
setIsOutputOpen(true);
}
}, [data.executionResults, data.status]);
}, [data.output_data, data.status]);
useEffect(() => {
setIsOutputOpen(data.isOutputOpen);
}, [data.isOutputOpen]);
useEffect(() => {
setIsAnyModalOpen?.(isModalOpen || isOutputModalOpen);
}, [isModalOpen, isOutputModalOpen, data, setIsAnyModalOpen]);
data.setIsAnyModalOpen?.(isModalOpen || isOutputModalOpen);
}, [isModalOpen, isOutputModalOpen, data]);
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);
};
@@ -124,16 +94,14 @@ export function CustomNode({ data, id, width, height }: NodeProps<CustomNode>) {
setIsAdvancedOpen(checked);
};
const generateOutputHandles = (
schema: BlockIORootSchema,
nodeType: BlockUIType,
) => {
if (
!schema?.properties ||
nodeType === BlockUIType.OUTPUT ||
nodeType === BlockUIType.NOTE
)
return null;
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 keys = Object.keys(schema.properties);
return keys.map((key) => (
<div key={key}>
@@ -147,137 +115,6 @@ export function CustomNode({ data, id, width, height }: NodeProps<CustomNode>) {
));
};
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));
@@ -303,47 +140,46 @@ export function CustomNode({ data, id, width, height }: NodeProps<CustomNode>) {
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: () => setHardcodedValues(data.hardcodedValues),
redo: () => setHardcodedValues(newValues),
undo: () => data.setHardcodedValues(data.hardcodedValues),
redo: () => data.setHardcodedValues(newValues),
});
}
setHardcodedValues(newValues);
data.setHardcodedValues(newValues);
const errors = data.errors || {};
// Remove error with the same key
setNestedProperty(errors, path, null);
setErrors({ ...errors });
data.setErrors({ ...errors });
};
// Helper function to parse keys with array indices
//TODO move to utils
const parseKeys = (key: string): ParsedKey[] => {
const splits = key.split(/_@_|_#_|_\$_|\./);
const regex = /(\w+)|\[(\d+)\]/g;
const keys: ParsedKey[] = [];
let match;
let currentKey: string | null = null;
splits.forEach((split) => {
const isInteger = /^\d+$/.test(split);
if (!isInteger) {
while ((match = regex.exec(key)) !== null) {
if (match[1]) {
if (currentKey !== null) {
keys.push({ key: currentKey });
}
currentKey = split;
} else {
currentKey = match[1];
} else if (match[2]) {
if (currentKey !== null) {
keys.push({ key: currentKey, index: parseInt(split, 10) });
keys.push({ key: currentKey, index: parseInt(match[2], 10) });
currentKey = null;
} else {
throw new Error("Invalid key format: array index without a key");
}
}
});
}
if (currentKey !== null) {
keys.push({ key: currentKey });
@@ -386,7 +222,7 @@ export function CustomNode({ data, id, width, height }: NodeProps<CustomNode>) {
console.log(`Opening modal for key: ${key}`);
setActiveKey(key);
const value = getValue(key);
setInputModalValue(
setModalValue(
typeof value === "object" ? JSON.stringify(value, null, 2) : value,
);
setIsModalOpen(true);
@@ -407,6 +243,19 @@ export function CustomNode({ data, id, width, height }: NodeProps<CustomNode>) {
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 = () => {
@@ -425,121 +274,29 @@ export function CustomNode({ data, id, width, height }: NodeProps<CustomNode>) {
}, [id, deleteElements]);
const copyNode = useCallback(() => {
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)
);
});
// This is a placeholder function. The actual copy functionality
// will be implemented by another team member.
console.log("Copy node:", id);
}, [id]);
return (
<div
className={`${data.uiType === BlockUIType.NOTE ? "w-[300px]" : "w-[500px]"} ${blockClasses} ${errorClass} ${statusClass} ${data.uiType === BlockUIType.NOTE ? "bg-yellow-100" : "bg-white"}`}
className={`custom-node dark-theme border rounded-xl shadow-md bg-white/[.9] ${data.status?.toLowerCase() ?? ""}`}
onMouseEnter={handleHovered}
onMouseLeave={handleMouseLeave}
data-id={`custom-node-${id}`}
>
<div
className={`mb-2 p-3 ${data.uiType === BlockUIType.NOTE ? "bg-yellow-100" : getPrimaryCategoryColor(data.categories)} rounded-t-xl`}
className={`mb-2 p-3 ${getPrimaryCategoryColor(data.categories)} rounded-t-xl`}
>
<div className="flex items-center justify-between">
<div className="font-roboto p-3 text-lg font-semibold">
<div className="p-3 text-lg font-semibold font-roboto">
{beautifyString(
data.blockType?.replace(/Block$/, "") || data.title,
)}
</div>
<SchemaTooltip description={data.description} />
</div>
<div className="flex gap-[5px]">
<div className="flex gap-[5px] ">
{isHovered && (
<>
<Button
@@ -562,80 +319,96 @@ export function CustomNode({ data, id, width, height }: NodeProps<CustomNode>) {
)}
</div>
</div>
{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 className="p-3 flex justify-between items-start gap-2">
<div>
{data.inputSchema &&
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>
)}
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>
</div>
)}
<div className="flex items-center pl-4 pb-4 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}
defaultValue={inputModalValue}
value={modalValue}
key={activeKey}
/>
<OutputModalComponent
isOpen={isOutputModalOpen}
onClose={() => setIsOutputModalOpen(false)}
executionResults={data.executionResults?.toReversed() || []}
value={modalValue}
/>
</div>
);
}
};
export default memo(CustomNode);

View File

@@ -1,92 +0,0 @@
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,107 +1,60 @@
import React, { FC, useEffect, useState } from "react";
import React, { FC, useEffect, useRef } 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;
title?: string;
defaultValue: string;
value: string;
}
const InputModalComponent: FC<ModalProps> = ({
isOpen,
onClose,
onSave,
title,
defaultValue,
value,
}) => {
const [tempValue, setTempValue] = useState(defaultValue);
const [isMaximized, setIsMaximized] = useState(false);
const [tempValue, setTempValue] = React.useState(value);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (isOpen) {
setTempValue(defaultValue);
setIsMaximized(false);
setTempValue(value);
if (textAreaRef.current) {
textAreaRef.current.select();
}
}
}, [isOpen, defaultValue]);
}, [isOpen, value]);
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;
}
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">
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>
<Textarea
className="h-full min-h-[200px] w-full resize-none"
ref={textAreaRef}
className="w-full h-[200px] p-2.5 rounded border border-[#dfdfdf] text-black bg-[#dfdfdf]"
value={tempValue}
onChange={(e) => setTempValue(e.target.value)}
/>
<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 className="flex justify-end gap-2.5 mt-2.5">
<Button onClick={onClose}>Cancel</Button>
<Button onClick={handleSave}>Save</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 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">
<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">
<Sheet>
<SheetTrigger asChild>
<Button
@@ -38,19 +38,19 @@ export async function NavBar() {
<nav className="grid gap-6 text-lg font-medium">
<Link
href="/"
className="flex flex-row gap-2 text-muted-foreground hover:text-foreground"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 "
>
<IconSquareActivity /> Monitor
</Link>
<Link
href="/build"
className="flex flex-row gap-2 text-muted-foreground hover:text-foreground"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2"
>
<IconWorkFlow /> Build
</Link>
<Link
href="/marketplace"
className="flex flex-row gap-2 text-muted-foreground hover:text-foreground"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2"
>
<IconPackage2 /> Marketplace
</Link>
@@ -60,25 +60,25 @@ export async function NavBar() {
<nav className="hidden md:flex md:flex-row md:items-center md:gap-5 lg:gap-6">
<Link
href="/"
className="flex flex-row items-center gap-2 text-muted-foreground hover:text-foreground"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 items-center"
>
<IconSquareActivity /> Monitor
</Link>
<Link
href="/build"
className="flex flex-row items-center gap-2 text-muted-foreground hover:text-foreground"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 items-center"
>
<IconWorkFlow /> Build
</Link>
<Link
href="/marketplace"
className="flex flex-row items-center gap-2 text-muted-foreground hover:text-foreground"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 items-center"
>
<IconPackage2 /> Marketplace
</Link>
</nav>
</div>
<div className="relative flex flex-1 justify-center">
<div className="flex-1 flex justify-center relative">
<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 flex-1 items-center justify-end gap-4">
<div className="flex items-center gap-4 flex-1 justify-end">
{isAvailable && !user && (
<Link
href="/login"
className="flex flex-row items-center gap-2 text-muted-foreground hover:text-foreground"
className="text-muted-foreground hover:text-foreground flex flex-row gap-2 items-center"
>
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 "@xyflow/react";
import { Handle, Position } from "reactflow";
import SchemaTooltip from "./SchemaTooltip";
type HandleProps = {
@@ -23,7 +23,7 @@ const NodeHandle: FC<HandleProps> = ({
string: "text",
number: "number",
boolean: "true/false",
object: "object",
object: "complex",
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-grow flex-col">
<span className="text-m green -mb-1 text-gray-900">
<div className="flex flex-col flex-grow">
<span className="text-m text-gray-900 -mb-1 green">
{schema.title || beautifyString(keyName)}
{isRequired ? "*" : ""}
</span>
@@ -42,7 +42,7 @@ const NodeHandle: FC<HandleProps> = ({
const dot = (
<div
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`}
className={`w-4 h-4 m-1 border-2 bg-white ${isConnected ? getTypeBgColor(schema.type || "any") : "border-gray-300"} rounded-full transition-colors duration-100 group-hover:bg-gray-300`}
/>
);
@@ -53,7 +53,7 @@ const NodeHandle: FC<HandleProps> = ({
type="target"
position={Position.Left}
id={keyName}
className="background-color: white; border: 2px solid black; width: 15px; height: 15px; border-radius: 50%; bottom: -7px; left: 20%; group -ml-[26px]"
className="group -ml-[26px] background-color: white; border: 2px solid black; width: 15px; height: 15px; border-radius: 50%; bottom: -7px; left: 20%;"
>
<div className="pointer-events-none flex items-center">
{dot}

View File

@@ -1,44 +1,48 @@
import React, { FC } from "react";
import React, { FC, useEffect } from "react";
import { createPortal } from "react-dom";
import { Button } from "./ui/button";
import { NodeExecutionResult } from "@/lib/autogpt-server-api/types";
import DataTable from "./DataTable";
import { Separator } from "@/components/ui/separator";
import { Textarea } from "./ui/textarea";
interface OutputModalProps {
isOpen: boolean;
onClose: () => void;
executionResults: {
execId: string;
data: NodeExecutionResult["output_data"];
}[];
value: string;
}
const OutputModalComponent: FC<OutputModalProps> = ({
isOpen,
onClose,
executionResults,
value,
}) => {
const [tempValue, setTempValue] = React.useState(value);
useEffect(() => {
if (isOpen) {
setTempValue(value);
}
}, [isOpen, value]);
if (!isOpen) {
return null;
}
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">
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">
<Button onClick={onClose}>Close</Button>
</div>
</div>
</div>
</div>,
document.body,
);
};

View File

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

View File

@@ -14,34 +14,21 @@ 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={user?.user_metadata["avatar_url"]}
alt="User Avatar"
/>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<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

@@ -1,27 +0,0 @@
// 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

@@ -14,17 +14,13 @@ const SchemaTooltip: React.FC<{ description?: string }> = ({ description }) => {
<TooltipProvider delayDuration={400}>
<Tooltip>
<TooltipTrigger asChild>
<Info className="rounded-full p-1 hover:bg-gray-300" size={24} />
<Info className="p-1 rounded-full hover:bg-gray-300" size={24} />
</TooltipTrigger>
<TooltipContent className="tooltip-content max-w-xs">
<TooltipContent className="max-w-xs tooltip-content">
<ReactMarkdown
components={{
a: ({ node, ...props }) => (
<a
target="_blank"
className="text-blue-400 underline"
{...props}
/>
<a className="text-blue-400 underline" {...props} />
),
}}
>

View File

@@ -41,16 +41,8 @@ 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 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>
<div className="fixed bottom-6 right-6 p-3 transition-all duration-300 ease-in-out z-50">
<Button
variant="default"
data-tally-open="3yx2L0"

View File

@@ -1,149 +0,0 @@
"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

@@ -1,67 +0,0 @@
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

@@ -1,36 +0,0 @@
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

@@ -1,113 +0,0 @@
"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

@@ -1,114 +0,0 @@
"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

@@ -1,85 +0,0 @@
"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,13 +21,8 @@ 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: fileSchema,
agentFile: z.instanceof(File),
agentName: z.string().min(1, "Agent name is required"),
agentDescription: z.string(),
importAsTemplate: z.boolean(),
@@ -170,7 +165,7 @@ export const AgentImportForm: React.FC<
<FormItem>
<FormLabel>Import as</FormLabel>
<FormControl>
<div className="flex items-center space-x-2">
<div className="flex space-x-2 items-center">
<span
className={
field.value ? "text-gray-400 dark:text-gray-600" : ""

View File

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

View File

@@ -1,5 +1,6 @@
.custom-node {
color: #000000;
width: 500px;
box-sizing: border-box;
transition: border-color 0.3s ease-in-out;
}
@@ -25,6 +26,7 @@
margin-bottom: 0px;
padding: 5px;
min-height: 44px;
width: 100%;
height: 100%;
}
@@ -99,6 +101,16 @@
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;
@@ -124,7 +136,7 @@
}
.queued {
border-color: #25e6e6; /* Cyan border for queued nodes */
border-color: #25e6e6; /* Cyanic border for failed nodes */
}
.custom-switch {

View File

@@ -16,17 +16,9 @@ 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;
}
/**
@@ -41,58 +33,32 @@ interface BlocksControlProps {
export const BlocksControl: React.FC<BlocksControlProps> = ({
blocks,
addBlock,
pinBlocksPopover,
}) => {
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
// 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)),
const filteredBlocks = blocks.filter((block: Block) =>
block.name.toLowerCase().includes(searchQuery.toLowerCase()),
);
return (
<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>
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon">
<IconToyBrick />
</Button>
</PopoverTrigger>
<PopoverContent
side="right"
sideOffset={22}
align="start"
className="w-[30rem] p-0"
data-id="blocks-control-popover-content"
className="w-96 p-0"
>
<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">
<CardHeader className="flex px-2 flex-col p-3 gap-x-8 gap-y-2">
<div className="justify-between items-center ">
<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"
className="text-base 2xl:text-xl font-semibold whitespace-nowrap text-black border-b-2 border-violet-500"
>
Blocks
</Label>
@@ -103,58 +69,28 @@ 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]"
data-id="blocks-control-scroll-area"
>
<ScrollArea className="h-[60vh]">
{filteredBlocks.map((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}`}
>
<div className="flex items-center justify-between m-3">
<div className="flex-1 min-w-0 mr-2">
<span className="font-medium truncate block">
{beautifyString(block.name)}
</span>
</div>
<SchemaTooltip description={block.description} />
<div
className="flex flex-shrink-0 items-center gap-1"
data-id={`block-tooltip-${block.id}`}
>
<div className="flex items-center gap-1 flex-shrink-0">
<Button
variant="ghost"
size="icon"
onClick={() => addBlock(block.id, block.name)}
aria-label="Add block"
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="rounded-radius flex flex-col items-center gap-8 px-2 sm:py-5">
<div className="flex flex-col items-center gap-8 px-2 sm:py-5 rounded-radius">
{children}
<Separator />
{controls.map((control, index) => (
@@ -54,7 +54,6 @@ 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,11 +10,6 @@ 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;
@@ -56,16 +51,11 @@ export const SaveControl = ({
return (
<Popover>
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon">
<IconSave />
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent side="right">Save</TooltipContent>
</Tooltip>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon">
<IconSave />
</Button>
</PopoverTrigger>
<PopoverContent side="right" sideOffset={15} align="start">
<Card className="border-none shadow-none">
<CardContent className="p-4">
@@ -88,7 +78,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 "@xyflow/react";
import { Edge } from "reactflow";
type ActionType =
| "ADD_NODE"

View File

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

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from "react";
import React, { useEffect, 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 = useMemo(() => new AutoGPTServerAPI(), []);
const api = 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, api]);
}, [flow.id]);
return (
<Card {...props}>
<CardHeader className="flex-row justify-between space-x-3 space-y-0">
<CardHeader className="flex-row justify-between space-y-0 space-x-3">
<div>
<CardTitle>
{flow.name} <span className="font-light">v{flow.version}</span>

View File

@@ -1,10 +1,9 @@
import React, { useCallback } from "react";
import AutoGPTServerAPI, { GraphMeta } from "@/lib/autogpt-server-api";
import React from "react";
import { 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 { Button, buttonVariants } from "@/components/ui/button";
import { IconSquare } from "@/components/ui/icons";
import { buttonVariants } from "@/components/ui/button";
import { Pencil2Icon } from "@radix-ui/react-icons";
import moment from "moment/moment";
import { FlowRunStatusBadge } from "@/components/monitor/FlowRunStatusBadge";
@@ -21,14 +20,9 @@ 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-x-3 space-y-0">
<CardHeader className="flex-row items-center justify-between space-y-0 space-x-3">
<div>
<CardTitle>
{flow.name} <span className="font-light">v{flow.version}</span>
@@ -40,19 +34,12 @@ export const FlowRunInfo: React.FC<
Run ID: <code>{flowRun.id}</code>
</p>
</div>
<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>
<Link
className={buttonVariants({ variant: "outline" })}
href={`/build?flowID=${flow.id}`}
>
<Pencil2Icon className="mr-2" /> Edit Agent
</Link>
</CardHeader>
<CardContent>
<p>

View File

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

View File

@@ -1,24 +0,0 @@
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

@@ -1,23 +0,0 @@
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

@@ -1,28 +0,0 @@
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 React, { FC, useCallback, useEffect, useState } from "react";
import { FC, useState } from "react";
import { Button } from "./ui/button";
import { Switch } from "./ui/switch";
import {
@@ -21,14 +21,11 @@ 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 };
@@ -40,7 +37,6 @@ const NodeObjectInputTree: FC<NodeObjectInputTreeProps> = ({
selfKey = "",
schema,
object,
connections,
handleInputClick,
handleInputChange,
errors,
@@ -49,7 +45,7 @@ const NodeObjectInputTree: FC<NodeObjectInputTreeProps> = ({
}) => {
object ??= ("default" in schema ? schema.default : null) ?? {};
return (
<div className={cn(className, "w-full flex-col")}>
<div className={cn(className, "flex-col w-full")}>
{displayName && <strong>{displayName}</strong>}
{Object.entries(schema.properties).map(([propKey, propSchema]) => {
const childKey = selfKey ? `${selfKey}.${propKey}` : propKey;
@@ -57,7 +53,7 @@ const NodeObjectInputTree: FC<NodeObjectInputTreeProps> = ({
return (
<div
key={propKey}
className="flex w-full flex-row justify-between space-y-2"
className="flex flex-row justify-between space-y-2 w-full"
>
<span className="mr-2 mt-3">
{propSchema.title || beautifyString(propKey)}
@@ -68,7 +64,6 @@ const NodeObjectInputTree: FC<NodeObjectInputTreeProps> = ({
propSchema={propSchema}
currentValue={object ? object[propKey] : undefined}
errors={errors}
connections={connections}
handleInputChange={handleInputChange}
handleInputClick={handleInputClick}
displayName={propSchema.title || beautifyString(propKey)}
@@ -87,7 +82,6 @@ export const NodeGenericInputField: FC<{
propSchema: BlockIOSubSchema;
currentValue?: any;
errors: NodeObjectInputTreeProps["errors"];
connections: NodeObjectInputTreeProps["connections"];
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
handleInputClick: NodeObjectInputTreeProps["handleInputClick"];
className?: string;
@@ -97,7 +91,6 @@ export const NodeGenericInputField: FC<{
propSchema,
currentValue,
errors,
connections,
handleInputChange,
handleInputClick,
className,
@@ -123,7 +116,6 @@ 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}
/>
@@ -139,7 +131,6 @@ export const NodeGenericInputField: FC<{
errors={errors}
className={className}
displayName={displayName}
connections={connections}
handleInputChange={handleInputChange}
/>
);
@@ -239,24 +230,10 @@ 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:`,
@@ -282,7 +259,6 @@ 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;
@@ -290,36 +266,21 @@ 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 }[]
>([]);
useEffect(
() => setKeyValuePairs(getPairValues()),
[connections, entries, schema.default, getPairValues],
{
key: string;
value: string | number | null;
}[]
>(
Object.entries(entries ?? schema.default ?? {}).map(([key, value]) => ({
key,
value: value,
})),
);
function updateKeyValuePairs(newPairs: typeof keyValuePairs) {
@@ -331,76 +292,54 @@ const NodeKeyValueInput: FC<{
}
function convertValueType(value: string): string | number | null {
if (
!schema.additionalProperties ||
schema.additionalProperties.type == "string"
)
return value;
if (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}>
{key && (
<NodeHandle
keyName={getEntryKey(key)}
schema={{ type: "string" }}
isConnected={isConnected(key)}
isRequired={false}
side="left"
<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,
}),
)
}
/>
)}
{!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>
)}
<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>
{errors[`${selfKey}.${key}`] && (
<span className="error-message">
{errors[`${selfKey}.${key}`]}
@@ -429,7 +368,6 @@ const NodeArrayInput: FC<{
schema: BlockIOArraySubSchema;
entries?: string[];
errors: { [key: string]: string | undefined };
connections: NodeObjectInputTreeProps["connections"];
handleInputChange: NodeObjectInputTreeProps["handleInputChange"];
handleInputClick: NodeObjectInputTreeProps["handleInputClick"];
className?: string;
@@ -439,7 +377,6 @@ const NodeArrayInput: FC<{
schema,
entries,
errors,
connections,
handleInputChange,
handleInputClick,
className,
@@ -453,52 +390,39 @@ 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 isConnected =
connections && connections.some((c) => c.targetHandle === entryKey);
const entryKey = `${selfKey}[${index}]`;
return (
<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>
<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}
/>
)}
<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>
@@ -530,7 +454,7 @@ const NodeStringInput: FC<{
}> = ({
selfKey,
schema,
value = "",
value,
error,
handleInputChange,
handleInputClick,
@@ -568,7 +492,7 @@ const NodeStringInput: FC<{
placeholder={
schema?.placeholder || `Enter ${beautifyString(displayName)}`
}
onBlur={(e) => handleInputChange(selfKey, e.target.value)}
onChange={(e) => handleInputChange(selfKey, e.target.value)}
className="pr-8 read-only:cursor-pointer read-only:text-gray-500"
/>
<Button
@@ -587,52 +511,6 @@ 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;
@@ -654,12 +532,14 @@ const NodeNumberInput: FC<{
displayName ??= schema.title || beautifyString(selfKey);
return (
<div className={className}>
<div className="nodrag flex items-center justify-between space-x-3">
<div className="flex items-center justify-between space-x-3 nodrag">
<Input
type="number"
id={selfKey}
value={value}
onBlur={(e) => handleInputChange(selfKey, parseFloat(e.target.value))}
onChange={(e) =>
handleInputChange(selfKey, parseFloat(e.target.value))
}
placeholder={
schema.placeholder || `Enter ${beautifyString(displayName)}`
}
@@ -690,7 +570,7 @@ const NodeBooleanInput: FC<{
value ??= schema.default ?? false;
return (
<div className={className}>
<div className="nodrag flex items-center">
<div className="flex items-center nodrag">
<Switch
checked={value}
onCheckedChange={(v) => handleInputChange(selfKey, v)}

View File

@@ -1,501 +0,0 @@
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

@@ -1,60 +0,0 @@
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

@@ -1,30 +0,0 @@
"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

@@ -1,155 +0,0 @@
"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

@@ -1,209 +0,0 @@
"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%] dark:border-neutral-800 dark:bg-neutral-950 sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-neutral-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg dark:border-neutral-800 dark:bg-neutral-950",
className,
)}
{...props}

View File

@@ -405,40 +405,6 @@ 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,30 +6,16 @@ export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ 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]);
({ className, type, ...props }, 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-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
type == "file" ? "pt-1.5 pb-0.5" : "", // fix alignment
className,
)}
ref={ref}
defaultValue={type !== "file" ? value : undefined}
{...props}
/>
);

View File

@@ -1,318 +0,0 @@
"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 dark:border-neutral-800 dark:ring-offset-neutral-950 dark:placeholder:text-neutral-400 dark:focus:ring-neutral-300 [&>span]:line-clamp-1",
"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",
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 dark:bg-neutral-800/50 [&>tr]:last:border-b-0",
"border-t bg-neutral-100/50 font-medium [&>tr]:last:border-b-0 dark:bg-neutral-800/50",
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 dark:text-neutral-400 [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
"h-10 px-2 text-left align-middle font-medium text-neutral-500 [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] dark:text-neutral-400",
className,
)}
{...props}

View File

@@ -1,130 +0,0 @@
"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

@@ -1,35 +0,0 @@
"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

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

View File

@@ -1,776 +0,0 @@
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

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

View File

@@ -15,17 +15,15 @@ export default class AutoGPTServerAPI {
private wsUrl: string;
private webSocket: WebSocket | null = null;
private wsConnecting: Promise<void> | null = null;
private wsMessageHandlers: Record<string, Set<(data: any) => void>> = {};
private wsMessageHandlers: { [key: string]: (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 = wsUrl;
this.wsUrl = `ws://${new URL(this.baseUrl).host}/ws`;
}
async createUser(): Promise<User> {
@@ -128,19 +126,16 @@ export default class AutoGPTServerAPI {
runID: string,
): Promise<NodeExecutionResult[]> {
return (await this._get(`/graphs/${graphID}/executions/${runID}`)).map(
parseNodeExecutionResultTimestamps,
(result: any) => ({
...result,
add_time: new Date(result.add_time),
queue_time: result.queue_time ? new Date(result.queue_time) : undefined,
start_time: result.start_time ? new Date(result.start_time) : undefined,
end_time: result.end_time ? new Date(result.end_time) : undefined,
}),
);
}
async 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);
}
@@ -195,12 +190,12 @@ export default class AutoGPTServerAPI {
this.webSocket = new WebSocket(wsUrlWithToken);
this.webSocket.onopen = () => {
console.debug("WebSocket connection established");
console.log("WebSocket connection established");
resolve();
};
this.webSocket.onclose = (event) => {
console.debug("WebSocket connection closed", event);
console.log("WebSocket connection closed", event);
this.webSocket = null;
};
@@ -210,13 +205,10 @@ export default class AutoGPTServerAPI {
};
this.webSocket.onmessage = (event) => {
const message: WebsocketMessage = JSON.parse(event.data);
if (message.method == "execution_event") {
message.data = parseNodeExecutionResultTimestamps(message.data);
const message = JSON.parse(event.data);
if (this.wsMessageHandlers[message.method]) {
this.wsMessageHandlers[message.method](message.data);
}
this.wsMessageHandlers[message.method]?.forEach((handler) =>
handler(message.data),
);
};
} catch (error) {
console.error("Error connecting to WebSocket:", error);
@@ -235,33 +227,21 @@ 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(() => {
callCount == 0
? this.sendWebSocketMessage(method, data, callCount + 1)
: setTimeout(
() => {
this.sendWebSocketMessage(method, data, callCount + 1);
},
2 ** (callCount - 1) * 1000,
);
});
this.connectWebSocket().then(() =>
this.sendWebSocketMessage(method, data),
);
}
}
onWebSocketMessage<M extends keyof WebsocketMessageTypeMap>(
method: M,
handler: (data: WebsocketMessageTypeMap[M]) => void,
): () => void {
this.wsMessageHandlers[method] ??= new Set();
this.wsMessageHandlers[method].add(handler);
// Return detacher
return () => this.wsMessageHandlers[method].delete(handler);
) {
this.wsMessageHandlers[method] = handler;
}
subscribeToExecution(graphId: string) {
@@ -284,22 +264,3 @@ type WebsocketMessageTypeMap = {
subscribe: { graph_id: string };
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

@@ -12,8 +12,6 @@ export type Block = {
categories: Category[];
inputSchema: BlockIORootSchema;
outputSchema: BlockIORootSchema;
staticOutput: boolean;
uiType: BlockUIType;
};
export type BlockIORootSchema = {
@@ -36,11 +34,10 @@ type BlockIOSimpleTypeSubSchema =
| BlockIOBooleanSubSchema
| BlockIONullSubSchema;
export type BlockIOSubSchemaMeta = {
type BlockIOSubSchemaMeta = {
title?: string;
description?: string;
placeholder?: string;
advanced?: boolean;
};
export type BlockIOObjectSubSchema = BlockIOSubSchemaMeta & {
@@ -120,10 +117,9 @@ export type Link = {
sink_id: string;
source_name: string;
sink_name: string;
is_static: boolean;
};
export type LinkCreatable = Omit<Link, "id" | "is_static"> & {
export type LinkCreatable = Omit<Link, "id"> & {
id?: string;
};
@@ -183,10 +179,3 @@ export type User = {
id: string;
email: string;
};
export enum BlockUIType {
STANDARD = "Standard",
INPUT = "Input",
OUTPUT = "Output",
NOTE = "Note",
}

View File

@@ -6,9 +6,6 @@ import {
AgentListResponse,
AgentDetailResponse,
AgentWithRank,
FeaturedAgentResponse,
UniqueCategoriesResponse,
AnalyticsEvent,
} from "./types";
export default class MarketplaceAPI {
@@ -46,7 +43,7 @@ export default class MarketplaceAPI {
pageSize: number = 10,
): Promise<AgentListResponse> {
return this._get(
`agents/top-downloads?page=${page}&page_size=${pageSize}`,
`/top-downloads/agents?page=${page}&page_size=${pageSize}`,
);
}
@@ -54,7 +51,7 @@ export default class MarketplaceAPI {
page: number = 1,
pageSize: number = 10,
): Promise<AgentListResponse> {
return this._get(`/agents/featured?page=${page}&page_size=${pageSize}`);
return this._get(`/featured/agents?page=${page}&page_size=${pageSize}`);
}
async searchAgents(
@@ -102,20 +99,6 @@ 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());
@@ -124,83 +107,10 @@ 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);
}
@@ -209,10 +119,6 @@ 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) {
@@ -228,7 +134,7 @@ export default class MarketplaceAPI {
}
private async _request(
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
method: "GET" | "POST" | "PUT" | "PATCH",
path: string,
payload?: { [key: string]: any },
) {

View File

@@ -43,22 +43,6 @@ 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>;
};
@@ -72,38 +56,3 @@ 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,7 +45,6 @@ export async function updateSession(request: NextRequest) {
const {
data: { user },
error,
} = await supabase.auth.getUser();
if (

View File

@@ -106,6 +106,8 @@ const exceptionMap: Record<string, string> = {
Url: "URL",
Http: "HTTP",
Json: "JSON",
"Dall E": "Dall-E",
"You Tube": "YouTube",
};
const applyExceptions = (str: string): string => {

View File

@@ -1,16 +0,0 @@
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

@@ -1,6 +1,6 @@
import pytest
from .depends import requires_admin_user, requires_user, verify_user
from .depends import verify_user, requires_admin_user, requires_user
def test_verify_user_no_payload():

View File

@@ -12,7 +12,6 @@ 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

@@ -1,9 +0,0 @@
from .config import configure_logging
from .filters import BelowLevelFilter
from .formatters import FancyConsoleFormatter
__all__ = [
"configure_logging",
"BelowLevelFilter",
"FancyConsoleFormatter",
]

View File

@@ -1,166 +0,0 @@
"""Logging module for Auto-GPT."""
import logging
import sys
from pathlib import Path
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from .filters import BelowLevelFilter
from .formatters import AGPTFormatter, StructuredLoggingFormatter
LOG_DIR = Path(__file__).parent.parent.parent.parent / "logs"
LOG_FILE = "activity.log"
DEBUG_LOG_FILE = "debug.log"
ERROR_LOG_FILE = "error.log"
SIMPLE_LOG_FORMAT = "%(asctime)s %(levelname)s %(title)s%(message)s"
DEBUG_LOG_FORMAT = (
"%(asctime)s %(levelname)s %(filename)s:%(lineno)d" " %(title)s%(message)s"
)
class LoggingConfig(BaseSettings):
level: str = Field(
default="INFO",
description="Logging level",
validation_alias="LOG_LEVEL",
)
enable_cloud_logging: bool = Field(
default=False,
description="Enable logging to Google Cloud Logging",
)
enable_file_logging: bool = Field(
default=False,
description="Enable logging to file",
)
# File output
log_dir: Path = Field(
default=LOG_DIR,
description="Log directory",
)
model_config = SettingsConfigDict(
env_prefix="",
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
@field_validator("level", mode="before")
@classmethod
def parse_log_level(cls, v):
if isinstance(v, str):
v = v.upper()
if v not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
raise ValueError(f"Invalid log level: {v}")
return v
return v
def configure_logging(force_cloud_logging: bool = False) -> None:
"""Configure the native logging module based on the LoggingConfig settings.
This function sets up logging handlers and formatters according to the
configuration specified in the LoggingConfig object. It supports various
logging outputs including console, file, cloud, and JSON logging.
The function uses the LoggingConfig object to determine which logging
features to enable and how to configure them. This includes setting
log levels, log formats, and output destinations.
No arguments are required as the function creates its own LoggingConfig
instance internally.
Note: This function is typically called at the start of the application
to set up the logging infrastructure.
"""
config = LoggingConfig()
log_handlers: list[logging.Handler] = []
# Cloud logging setup
if config.enable_cloud_logging or force_cloud_logging:
import google.cloud.logging
from google.cloud.logging.handlers import CloudLoggingHandler
from google.cloud.logging_v2.handlers.transports.sync import SyncTransport
client = google.cloud.logging.Client()
cloud_handler = CloudLoggingHandler(
client,
name="autogpt_logs",
transport=SyncTransport,
)
cloud_handler.setLevel(config.level)
cloud_handler.setFormatter(StructuredLoggingFormatter())
log_handlers.append(cloud_handler)
print("Cloud logging enabled")
else:
# Console output handlers
stdout = logging.StreamHandler(stream=sys.stdout)
stdout.setLevel(config.level)
stdout.addFilter(BelowLevelFilter(logging.WARNING))
if config.level == logging.DEBUG:
stdout.setFormatter(AGPTFormatter(DEBUG_LOG_FORMAT))
else:
stdout.setFormatter(AGPTFormatter(SIMPLE_LOG_FORMAT))
stderr = logging.StreamHandler()
stderr.setLevel(logging.WARNING)
if config.level == logging.DEBUG:
stderr.setFormatter(AGPTFormatter(DEBUG_LOG_FORMAT))
else:
stderr.setFormatter(AGPTFormatter(SIMPLE_LOG_FORMAT))
log_handlers += [stdout, stderr]
print("Console logging enabled")
# File logging setup
if config.enable_file_logging:
# create log directory if it doesn't exist
if not config.log_dir.exists():
config.log_dir.mkdir(parents=True, exist_ok=True)
print(f"Log directory: {config.log_dir}")
# Activity log handler (INFO and above)
activity_log_handler = logging.FileHandler(
config.log_dir / LOG_FILE, "a", "utf-8"
)
activity_log_handler.setLevel(config.level)
activity_log_handler.setFormatter(
AGPTFormatter(SIMPLE_LOG_FORMAT, no_color=True)
)
log_handlers.append(activity_log_handler)
if config.level == logging.DEBUG:
# Debug log handler (all levels)
debug_log_handler = logging.FileHandler(
config.log_dir / DEBUG_LOG_FILE, "a", "utf-8"
)
debug_log_handler.setLevel(logging.DEBUG)
debug_log_handler.setFormatter(
AGPTFormatter(DEBUG_LOG_FORMAT, no_color=True)
)
log_handlers.append(debug_log_handler)
# Error log handler (ERROR and above)
error_log_handler = logging.FileHandler(
config.log_dir / ERROR_LOG_FILE, "a", "utf-8"
)
error_log_handler.setLevel(logging.ERROR)
error_log_handler.setFormatter(AGPTFormatter(DEBUG_LOG_FORMAT, no_color=True))
log_handlers.append(error_log_handler)
print("File logging enabled")
# Configure the root logger
logging.basicConfig(
format=DEBUG_LOG_FORMAT if config.level == logging.DEBUG else SIMPLE_LOG_FORMAT,
level=config.level,
handlers=log_handlers,
)

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