Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f1d2745fd | ||
|
|
b918ffdf76 | ||
|
|
3ba85d9c37 | ||
|
|
e198d858b9 | ||
|
|
c2df6a0a5c | ||
|
|
509e03d46d | ||
|
|
d09a9ca046 | ||
|
|
17524e5dff | ||
|
|
840e55438a | ||
|
|
0b722c9852 | ||
|
|
7ff7a1455c | ||
|
|
b4939b0fca | ||
|
|
eb7bb06ef3 | ||
|
|
53e2c7016c | ||
|
|
27a367972b | ||
|
|
c9087b1918 | ||
|
|
772263f7d7 | ||
|
|
e2d3483de2 | ||
|
|
62a47cc7d2 | ||
|
|
fe778b3eb9 | ||
|
|
4e1f663787 | ||
|
|
9ef4835344 | ||
|
|
dc01331696 | ||
|
|
f1f2ba8823 | ||
|
|
027c096377 | ||
|
|
c4a4e5fd68 | ||
|
|
956d16a854 | ||
|
|
482c1693d1 | ||
|
|
e2e756156f | ||
|
|
cf24a7ce5d | ||
|
|
3e8c322e79 | ||
|
|
9135aa59d6 | ||
|
|
ef4e4ee1c7 | ||
|
|
7a9d3e1477 | ||
|
|
b619d88d4e | ||
|
|
2c7e5d0528 | ||
|
|
c469bf1998 | ||
|
|
25db7e8a9a | ||
|
|
d01644c00c | ||
|
|
86258efcbd | ||
|
|
24d7950d13 | ||
|
|
f000a08701 | ||
|
|
257d50a584 | ||
|
|
9f176bf0e0 | ||
|
|
21e67e43a3 | ||
|
|
c916b7a660 | ||
|
|
c6916eabc5 | ||
|
|
7f35a9a6bc | ||
|
|
6d7b1194d0 | ||
|
|
1bcd76595c | ||
|
|
6234e3838f | ||
|
|
ddb7650f59 | ||
|
|
43b2c03c91 | ||
|
|
eebe995826 | ||
|
|
5e854b9d1d | ||
|
|
0c96e42f51 | ||
|
|
2d98cb715f | ||
|
|
16df8a9a1b | ||
|
|
ecdd0cc917 | ||
|
|
f4878452f6 | ||
|
|
8654988f57 | ||
|
|
09eea12250 | ||
|
|
07d4fb18f3 | ||
|
|
0b12acd4ac | ||
|
|
a33881759c | ||
|
|
139497e1fa | ||
|
|
2bbe2c90ac | ||
|
|
e200ce6490 | ||
|
|
13b68095e6 | ||
|
|
7940876e6f | ||
|
|
17d72238c7 | ||
|
|
22a771abd8 | ||
|
|
1656fda8da | ||
|
|
6bf0ea52e0 | ||
|
|
add128f4d5 | ||
|
|
958eeec4a6 | ||
|
|
18c6865926 | ||
|
|
3622260c11 | ||
|
|
0ca6b4f3e9 | ||
|
|
2a80bdf7a3 | ||
|
|
198eb57032 | ||
|
|
9ab001c35e | ||
|
|
0dbf6d1c13 | ||
|
|
6cfff4cc95 | ||
|
|
98c559e1ce | ||
|
|
6a9f329def | ||
|
|
b29d8d108e | ||
|
|
72e4b7865a | ||
|
|
ef5e84859d | ||
|
|
ae62a557d3 | ||
|
|
86de80a998 | ||
|
|
2bac2b3824 | ||
|
|
3185233233 | ||
|
|
48597bdf30 | ||
|
|
b53746dd1f | ||
|
|
ebbbf62df5 | ||
|
|
d0f40e7d35 | ||
|
|
15a4ec7e33 | ||
|
|
6c0dfae9bf | ||
|
|
48d15a6128 | ||
|
|
7ddbce89d0 | ||
|
|
cf07f732c2 | ||
|
|
897d3dc9ea | ||
|
|
8438e75dff | ||
|
|
a099575620 | ||
|
|
37c17c9e3d | ||
|
|
7e013787ed |
@@ -40,6 +40,12 @@ tests/
|
|||||||
.flake8
|
.flake8
|
||||||
requirements-dev.txt
|
requirements-dev.txt
|
||||||
|
|
||||||
|
# Python specific - don't exclude frontend
|
||||||
|
!frontend/
|
||||||
|
frontend/node_modules/
|
||||||
|
frontend/.next/
|
||||||
|
frontend/dist/
|
||||||
|
|
||||||
# Ambiente virtual
|
# Ambiente virtual
|
||||||
venv/
|
venv/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
@@ -54,7 +60,7 @@ dist/
|
|||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
lib/
|
# lib/
|
||||||
lib64/
|
lib64/
|
||||||
parts/
|
parts/
|
||||||
sdist/
|
sdist/
|
||||||
|
|||||||
16
.env.example
16
.env.example
@@ -6,6 +6,9 @@ API_URL="http://localhost:8000"
|
|||||||
ORGANIZATION_NAME="Evo AI"
|
ORGANIZATION_NAME="Evo AI"
|
||||||
ORGANIZATION_URL="https://evoai.evoapicloud.com"
|
ORGANIZATION_URL="https://evoai.evoapicloud.com"
|
||||||
|
|
||||||
|
# AI Engine configuration: "adk" or "crewai"
|
||||||
|
AI_ENGINE="adk"
|
||||||
|
|
||||||
# Database settings
|
# Database settings
|
||||||
POSTGRES_CONNECTION_STRING="postgresql://postgres:root@localhost:5432/evo_ai"
|
POSTGRES_CONNECTION_STRING="postgresql://postgres:root@localhost:5432/evo_ai"
|
||||||
|
|
||||||
@@ -34,9 +37,22 @@ JWT_EXPIRATION_TIME=3600
|
|||||||
# Encryption key for API keys
|
# Encryption key for API keys
|
||||||
ENCRYPTION_KEY="your-encryption-key"
|
ENCRYPTION_KEY="your-encryption-key"
|
||||||
|
|
||||||
|
# Email provider settings
|
||||||
|
EMAIL_PROVIDER="sendgrid"
|
||||||
|
|
||||||
# SendGrid
|
# SendGrid
|
||||||
SENDGRID_API_KEY="your-sendgrid-api-key"
|
SENDGRID_API_KEY="your-sendgrid-api-key"
|
||||||
EMAIL_FROM="noreply@yourdomain.com"
|
EMAIL_FROM="noreply@yourdomain.com"
|
||||||
|
|
||||||
|
# SMTP settings
|
||||||
|
SMTP_HOST="your-smtp-host"
|
||||||
|
SMTP_FROM="noreply-smtp@yourdomain.com"
|
||||||
|
SMTP_USER="your-smtp-username"
|
||||||
|
SMTP_PASSWORD="your-smtp-password"
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USE_TLS=true
|
||||||
|
SMTP_USE_SSL=false
|
||||||
|
|
||||||
APP_URL="https://yourdomain.com"
|
APP_URL="https://yourdomain.com"
|
||||||
|
|
||||||
LANGFUSE_PUBLIC_KEY="your-langfuse-public-key"
|
LANGFUSE_PUBLIC_KEY="your-langfuse-public-key"
|
||||||
|
|||||||
149
.github/workflows/build-and-deploy.yml
vendored
Normal file
149
.github/workflows/build-and-deploy.yml
vendored
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
name: Build and Deploy Docker Images
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- "*.*.*"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
detect-changes:
|
||||||
|
name: Detect Changes
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
backend-changed: ${{ steps.changes.outputs.backend }}
|
||||||
|
frontend-changed: ${{ steps.changes.outputs.frontend }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Detect changes
|
||||||
|
id: changes
|
||||||
|
uses: dorny/paths-filter@v2
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
backend:
|
||||||
|
- 'src/**'
|
||||||
|
- 'migrations/**'
|
||||||
|
- 'scripts/**'
|
||||||
|
- 'Dockerfile'
|
||||||
|
- 'pyproject.toml'
|
||||||
|
- 'alembic.ini'
|
||||||
|
- 'conftest.py'
|
||||||
|
- 'setup.py'
|
||||||
|
- 'Makefile'
|
||||||
|
- '.dockerignore'
|
||||||
|
frontend:
|
||||||
|
- 'frontend/**'
|
||||||
|
|
||||||
|
build-backend:
|
||||||
|
name: Build Backend Image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: detect-changes
|
||||||
|
if: needs.detect-changes.outputs.backend-changed == 'true' || github.event_name == 'push'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: evoapicloud/evo-ai
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Image digest
|
||||||
|
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||||
|
|
||||||
|
build-frontend:
|
||||||
|
name: Build Frontend Image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: detect-changes
|
||||||
|
if: needs.detect-changes.outputs.frontend-changed == 'true' || github.event_name == 'push'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: evoapicloud/evo-ai-frontend
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ./frontend
|
||||||
|
file: ./frontend/Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
build-args: |
|
||||||
|
NEXT_PUBLIC_API_URL=${{ vars.NEXT_PUBLIC_API_URL || 'https://api-evoai.evoapicloud.com' }}
|
||||||
|
|
||||||
|
- name: Image digest
|
||||||
|
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||||
139
.github/workflows/build-homolog.yml
vendored
Normal file
139
.github/workflows/build-homolog.yml
vendored
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
name: Build Homolog Images
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- homolog
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
detect-changes:
|
||||||
|
name: Detect Changes
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
backend-changed: ${{ steps.changes.outputs.backend }}
|
||||||
|
frontend-changed: ${{ steps.changes.outputs.frontend }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Detect changes
|
||||||
|
id: changes
|
||||||
|
uses: dorny/paths-filter@v2
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
backend:
|
||||||
|
- 'src/**'
|
||||||
|
- 'migrations/**'
|
||||||
|
- 'scripts/**'
|
||||||
|
- 'Dockerfile'
|
||||||
|
- 'pyproject.toml'
|
||||||
|
- 'alembic.ini'
|
||||||
|
- 'conftest.py'
|
||||||
|
- 'setup.py'
|
||||||
|
- 'Makefile'
|
||||||
|
- '.dockerignore'
|
||||||
|
frontend:
|
||||||
|
- 'frontend/**'
|
||||||
|
|
||||||
|
build-backend-homolog:
|
||||||
|
name: Build Backend Homolog
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: detect-changes
|
||||||
|
if: needs.detect-changes.outputs.backend-changed == 'true' || github.event_name == 'push'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: evoapicloud/evo-ai
|
||||||
|
tags: |
|
||||||
|
type=raw,value=homolog
|
||||||
|
type=raw,value=homolog-{{sha}}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Image digest
|
||||||
|
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||||
|
|
||||||
|
build-frontend-homolog:
|
||||||
|
name: Build Frontend Homolog
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: detect-changes
|
||||||
|
if: needs.detect-changes.outputs.frontend-changed == 'true' || github.event_name == 'push'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: evoapicloud/evo-ai-frontend
|
||||||
|
tags: |
|
||||||
|
type=raw,value=homolog
|
||||||
|
type=raw,value=homolog-{{sha}}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ./frontend
|
||||||
|
file: ./frontend/Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
build-args: |
|
||||||
|
NEXT_PUBLIC_API_URL=${{ vars.NEXT_PUBLIC_API_URL_HOMOLOG || 'https://api-homolog-evoai.evoapicloud.com' }}
|
||||||
|
|
||||||
|
- name: Image digest
|
||||||
|
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||||
53
.github/workflows/docker-image.yml
vendored
53
.github/workflows/docker-image.yml
vendored
@@ -1,53 +0,0 @@
|
|||||||
name: Docker Image CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["main", "develop"]
|
|
||||||
pull_request:
|
|
||||||
branches: ["main", "develop"]
|
|
||||||
|
|
||||||
env:
|
|
||||||
REGISTRY: ghcr.io
|
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ env.REGISTRY }}
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
||||||
tags: |
|
|
||||||
type=raw,value=develop,enable=${{ github.ref == format('refs/heads/{0}', 'develop') }}
|
|
||||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
|
||||||
type=sha
|
|
||||||
type=ref,event=branch
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,7 +11,7 @@ dist/
|
|||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
lib/
|
# lib/
|
||||||
lib64/
|
lib64/
|
||||||
parts/
|
parts/
|
||||||
sdist/
|
sdist/
|
||||||
|
|||||||
46
CHANGELOG.md
46
CHANGELOG.md
@@ -5,7 +5,51 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [0.0.7] - 2025-05-14
|
## [0.1.0] - 2025-05-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Export and Import Agents
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- A2A implementation updated to version 0.2.1 (https://google.github.io/A2A/specification/#agent2agent-a2a-protocol-specification)
|
||||||
|
- Frontend redesign
|
||||||
|
- Fixed message order
|
||||||
|
|
||||||
|
## [0.0.11] - 2025-05-16
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Fixes in email service and client service
|
||||||
|
|
||||||
|
## [0.0.10] - 2025-05-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add Task Agent for structured single-task execution
|
||||||
|
- Improve context management in agent execution
|
||||||
|
- Add file support for A2A protocol (Agent-to-Agent) endpoints
|
||||||
|
- Implement multimodal content processing in A2A messages
|
||||||
|
- Add SMTP email provider support as alternative to SendGrid
|
||||||
|
|
||||||
|
## [0.0.9] - 2025-05-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add API key sharing and flexible authentication for chat routes
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Enhance user authentication with detailed error handling
|
||||||
|
|
||||||
|
## [0.0.8] - 2025-05-13
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Update author information in multiple files
|
||||||
|
|
||||||
|
## [0.0.7] - 2025-05-13
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
|||||||
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
contato@evolution-api.com.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
||||||
141
CONTRIBUTING.md
Normal file
141
CONTRIBUTING.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# Contributing to Evo AI
|
||||||
|
|
||||||
|
We welcome contributions from the community! Please follow the guidelines below to help us maintain a high-quality, consistent, and secure project.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Prerequisites
|
||||||
|
|
||||||
|
- **Backend**: Python 3.10+, PostgreSQL 13+, Redis 6+, Git, Make
|
||||||
|
- **Frontend**: Node.js 18+, pnpm (recommended), or npm/yarn
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Setting Up the Development Environment
|
||||||
|
|
||||||
|
### 1. Clone the Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/EvolutionAPI/evo-ai.git
|
||||||
|
cd evo-ai
|
||||||
|
````
|
||||||
|
|
||||||
|
### 2. Backend Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make venv
|
||||||
|
source venv/bin/activate # On Linux/Mac
|
||||||
|
# Or: venv\Scripts\activate # On Windows
|
||||||
|
|
||||||
|
make install-dev
|
||||||
|
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your local settings
|
||||||
|
|
||||||
|
make alembic-upgrade
|
||||||
|
make seed-all
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Frontend Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
pnpm install # Or: npm install / yarn install
|
||||||
|
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your API URL, e.g., NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Running the Application
|
||||||
|
|
||||||
|
* **Backend**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make run
|
||||||
|
# Backend: http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
* **Frontend**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
pnpm dev
|
||||||
|
# Frontend: http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Issue and Pull Request Guidelines
|
||||||
|
|
||||||
|
* **Check for existing issues** before creating a new one.
|
||||||
|
* **Describe bugs or feature requests** clearly with steps to reproduce (if applicable).
|
||||||
|
* **Pull Requests** should:
|
||||||
|
|
||||||
|
* Reference relevant issues (e.g., `Fixes #123`)
|
||||||
|
* Focus on one change at a time
|
||||||
|
* Include tests where applicable
|
||||||
|
* Pass linting and formatting checks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧑💻 Code Standards
|
||||||
|
|
||||||
|
* **All code comments, docstrings, and log messages must be in English**
|
||||||
|
* **Variable, function, and class names**: English only
|
||||||
|
* **API error messages and documentation**: English
|
||||||
|
* **Commit messages**: English and follow [Conventional Commits](https://www.conventionalcommits.org/)
|
||||||
|
|
||||||
|
* Example: `feat(auth): add password reset functionality`
|
||||||
|
* **Indentation**: 4 spaces
|
||||||
|
* **Max line length**: 79 characters
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 Project Structure and Best Practices
|
||||||
|
|
||||||
|
* Follow the directory structure and naming conventions described in `.cursorrules`.
|
||||||
|
* **Tests** should be placed under `tests/` and follow the `test_*` naming convention.
|
||||||
|
* All routes require input validation using Pydantic schemas.
|
||||||
|
* Use transactions for database operations affecting multiple records.
|
||||||
|
* Document all public functions and classes.
|
||||||
|
* Keep `.env.example` updated when adding environment variables.
|
||||||
|
* Sensitive values must be set via environment variables and never hard-coded.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐳 Docker Development
|
||||||
|
|
||||||
|
* **Build and start stack:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make docker-build
|
||||||
|
make docker-up
|
||||||
|
```
|
||||||
|
* **Seed database:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make docker-seed
|
||||||
|
```
|
||||||
|
* **Stop stack:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make docker-down
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Contributor License
|
||||||
|
|
||||||
|
By contributing to this repository, you agree that your contributions will be licensed under the [Apache License 2.0](./LICENSE).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💬 Community and Support
|
||||||
|
|
||||||
|
* [WhatsApp Group](https://evolution-api.com/whatsapp)
|
||||||
|
* [Discord Community](https://evolution-api.com/discord)
|
||||||
|
* [Official Documentation](https://doc.evolution-api.com)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Thank you for contributing to Evo AI!
|
||||||
@@ -34,4 +34,4 @@ ENV PORT=8000 \
|
|||||||
# Expose port
|
# Expose port
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
CMD alembic upgrade head && uvicorn src.main:app --host $HOST --port $PORT
|
CMD alembic upgrade head && python -m scripts.run_seeders && uvicorn src.main:app --host $HOST --port $PORT
|
||||||
2
Makefile
2
Makefile
@@ -18,7 +18,7 @@ alembic-downgrade:
|
|||||||
|
|
||||||
# Command to run the server
|
# Command to run the server
|
||||||
run:
|
run:
|
||||||
uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload --env-file .env
|
uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload --env-file .env --reload-exclude frontend/ --reload-exclude "*.log" --reload-exclude "*.tmp"
|
||||||
|
|
||||||
# Command to run the server in production mode
|
# Command to run the server in production mode
|
||||||
run-prod:
|
run-prod:
|
||||||
|
|||||||
75
SECURITY.md
Normal file
75
SECURITY.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
We aim to support the latest stable release of Evo AI and apply security updates as soon as possible. Please use the most recent version for the best security.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you discover a security vulnerability in Evo AI, **please report it privately** and responsibly. Do **not** open a public issue.
|
||||||
|
|
||||||
|
**To report a vulnerability:**
|
||||||
|
|
||||||
|
- Email: [contato@evolution-api.com](mailto:contato@evolution-api.com)
|
||||||
|
- Include as much detail as possible, including:
|
||||||
|
- Steps to reproduce the issue
|
||||||
|
- Potential impact
|
||||||
|
- Your suggestions (if any) for remediation
|
||||||
|
|
||||||
|
You will receive a response as soon as possible. We may request additional information to fully understand and address the issue.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
- **Keep your installation up to date.**
|
||||||
|
Always use the latest stable version and regularly check for updates.
|
||||||
|
|
||||||
|
- **Environment Variables:**
|
||||||
|
Store all secrets, credentials, and keys in environment variables or secrets managers.
|
||||||
|
Never commit sensitive information to the repository.
|
||||||
|
|
||||||
|
- **Authentication:**
|
||||||
|
Evo AI uses JWT authentication with expiration, email verification, and account lockout for brute-force protection.
|
||||||
|
|
||||||
|
- **Passwords:**
|
||||||
|
All passwords are securely hashed with bcrypt and random salt.
|
||||||
|
|
||||||
|
- **Access Control:**
|
||||||
|
Access to sensitive endpoints is protected via role-based checks and resource ownership verification.
|
||||||
|
|
||||||
|
- **Audit Logs:**
|
||||||
|
Important administrative actions are logged for traceability.
|
||||||
|
|
||||||
|
- **Input Validation:**
|
||||||
|
All inputs are validated using Pydantic schemas to prevent injection attacks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Responsible Disclosure
|
||||||
|
|
||||||
|
Please give us a reasonable time to investigate and address any reported security issues before any public disclosure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Security Features
|
||||||
|
|
||||||
|
- JWT tokens with limited lifetime
|
||||||
|
- Secure password hashing (bcrypt)
|
||||||
|
- Email verification with one-time tokens
|
||||||
|
- Account lockout after multiple failed login attempts
|
||||||
|
- Resource-based access control
|
||||||
|
- Strict input validation for all APIs
|
||||||
|
- Separation between regular and administrative users
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
All security contributions are made under the [Apache License 2.0](./LICENSE).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Thank you for helping keep Evo AI and its users safe!
|
||||||
41
conftest.py
41
conftest.py
@@ -1,3 +1,32 @@
|
|||||||
|
"""
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: conftest.py │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
@@ -22,11 +51,11 @@ TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engin
|
|||||||
def db_session():
|
def db_session():
|
||||||
"""Creates a fresh database session for each test."""
|
"""Creates a fresh database session for each test."""
|
||||||
Base.metadata.create_all(bind=engine) # Create tables
|
Base.metadata.create_all(bind=engine) # Create tables
|
||||||
|
|
||||||
connection = engine.connect()
|
connection = engine.connect()
|
||||||
transaction = connection.begin()
|
transaction = connection.begin()
|
||||||
session = TestingSessionLocal(bind=connection)
|
session = TestingSessionLocal(bind=connection)
|
||||||
|
|
||||||
# Use our test database instead of the standard one
|
# Use our test database instead of the standard one
|
||||||
def override_get_db():
|
def override_get_db():
|
||||||
try:
|
try:
|
||||||
@@ -34,11 +63,11 @@ def db_session():
|
|||||||
session.commit()
|
session.commit()
|
||||||
finally:
|
finally:
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
app.dependency_overrides[get_db] = override_get_db
|
app.dependency_overrides[get_db] = override_get_db
|
||||||
|
|
||||||
yield session # The test will run here
|
yield session # The test will run here
|
||||||
|
|
||||||
# Teardown
|
# Teardown
|
||||||
transaction.rollback()
|
transaction.rollback()
|
||||||
connection.close()
|
connection.close()
|
||||||
@@ -50,4 +79,4 @@ def db_session():
|
|||||||
def client(db_session):
|
def client(db_session):
|
||||||
"""Creates a FastAPI TestClient with database session fixture."""
|
"""Creates a FastAPI TestClient with database session fixture."""
|
||||||
with TestClient(app) as test_client:
|
with TestClient(app) as test_client:
|
||||||
yield test_client
|
yield test_client
|
||||||
|
|||||||
@@ -2,19 +2,19 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
api:
|
api:
|
||||||
build: .
|
# image: evoapicloud/evo-ai:latest Use this image to pull from the repo
|
||||||
container_name: evo-ai-api
|
image: evoai-api:latest # Use this image for local builds
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
- postgres
|
||||||
condition: service_healthy
|
- redis
|
||||||
redis:
|
|
||||||
condition: service_healthy
|
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_CONNECTION_STRING: postgresql://postgres:${POSTGRES_PASSWORD:-postgres}@postgres:5432/evo_ai
|
POSTGRES_CONNECTION_STRING: postgresql://postgres:${POSTGRES_PASSWORD:-postgres}@postgres:5432/evo_ai
|
||||||
REDIS_HOST: redis
|
REDIS_HOST: redis
|
||||||
REDIS_PORT: 6379
|
REDIS_PORT: ${REDIS_PORT:-6379}
|
||||||
REDIS_PASSWORD: ${REDIS_PASSWORD:-""}
|
REDIS_PASSWORD: ${REDIS_PASSWORD:-""}
|
||||||
REDIS_SSL: "false"
|
REDIS_SSL: "false"
|
||||||
REDIS_KEY_PREFIX: "a2a:"
|
REDIS_KEY_PREFIX: "a2a:"
|
||||||
@@ -28,7 +28,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
- ./static:/app/static
|
- ./static:/app/static
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000/"]
|
test: ["CMD", "curl", "-f", "http://localhost:8000/"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -40,12 +39,9 @@ services:
|
|||||||
limits:
|
limits:
|
||||||
cpus: "1"
|
cpus: "1"
|
||||||
memory: 1G
|
memory: 1G
|
||||||
networks:
|
|
||||||
- evo-network
|
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:14-alpine
|
image: postgres:14-alpine
|
||||||
container_name: evo-ai-postgres
|
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||||
@@ -54,15 +50,12 @@ services:
|
|||||||
- "${POSTGRES_PORT:-5432}:5432"
|
- "${POSTGRES_PORT:-5432}:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
start_period: 10s
|
start_period: 10s
|
||||||
networks:
|
|
||||||
- evo-network
|
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
@@ -71,8 +64,12 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:alpine
|
image: redis:alpine
|
||||||
container_name: evo-ai-redis
|
command:
|
||||||
command: redis-server --appendonly yes ${REDIS_PASSWORD:+--requirepass ${REDIS_PASSWORD}}
|
- redis-server
|
||||||
|
- --appendonly
|
||||||
|
- "yes"
|
||||||
|
- --requirepass
|
||||||
|
- "${REDIS_PASSWORD}"
|
||||||
ports:
|
ports:
|
||||||
- "${REDIS_PORT:-6379}:6379"
|
- "${REDIS_PORT:-6379}:6379"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -82,9 +79,6 @@ services:
|
|||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 30s
|
timeout: 30s
|
||||||
retries: 50
|
retries: 50
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- evo-network
|
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
@@ -96,8 +90,3 @@ volumes:
|
|||||||
name: ${POSTGRES_VOLUME_NAME:-evo-ai-postgres-data}
|
name: ${POSTGRES_VOLUME_NAME:-evo-ai-postgres-data}
|
||||||
redis_data:
|
redis_data:
|
||||||
name: ${REDIS_VOLUME_NAME:-evo-ai-redis-data}
|
name: ${REDIS_VOLUME_NAME:-evo-ai-redis-data}
|
||||||
|
|
||||||
networks:
|
|
||||||
evo-network:
|
|
||||||
name: ${NETWORK_NAME:-evo-network}
|
|
||||||
driver: bridge
|
|
||||||
|
|||||||
120
frontend/.cursorrules
Normal file
120
frontend/.cursorrules
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# Next.js Project Rules
|
||||||
|
|
||||||
|
## Language
|
||||||
|
- All code, comments, documentation, commits, and PRs MUST be written in English.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Folder Structure
|
||||||
|
- `/app`: App router pages and API routes
|
||||||
|
- Route-specific components should be placed in their respective route folders
|
||||||
|
- `/components`: Reusable UI components
|
||||||
|
- `/ui`: Shadcn UI components and their derivatives
|
||||||
|
- `/contexts`: React Context providers
|
||||||
|
- `/hooks`: Custom React hooks
|
||||||
|
- `/lib`: Utility functions and configuration
|
||||||
|
- `/public`: Static assets
|
||||||
|
- `/services`: API service functions
|
||||||
|
- `/styles`: Global styles
|
||||||
|
- `/types`: TypeScript type definitions
|
||||||
|
|
||||||
|
### Component Guidelines
|
||||||
|
- Use functional components with TypeScript
|
||||||
|
- Use the `.tsx` extension for React components
|
||||||
|
- Follow a logical naming convention:
|
||||||
|
- Complex components: Use PascalCase and create folders with an index.tsx file
|
||||||
|
- Simple components: Single PascalCase named files
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
- Use React Context for global state
|
||||||
|
- Use React hooks for local state
|
||||||
|
- Avoid prop drilling more than 2 levels deep
|
||||||
|
|
||||||
|
### API & Data Fetching
|
||||||
|
- Use API service modules in `/services` directory
|
||||||
|
- Implement proper error handling and loading states
|
||||||
|
- Use React Query or SWR for complex data fetching where appropriate
|
||||||
|
|
||||||
|
## Development Patterns
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
- Maintain type safety - avoid using `any` type
|
||||||
|
- Write self-documenting code with descriptive names
|
||||||
|
- Keep components focused on a single responsibility
|
||||||
|
- Extract complex logic into custom hooks
|
||||||
|
- Follow DRY (Don't Repeat Yourself) principle
|
||||||
|
|
||||||
|
### CSS & Styling
|
||||||
|
- Use Tailwind CSS for styling
|
||||||
|
- Use Shadcn UI components as base building blocks
|
||||||
|
- Maintain consistent spacing and sizing
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Avoid unnecessary re-renders
|
||||||
|
- Optimize images and assets
|
||||||
|
- Implement code splitting where appropriate
|
||||||
|
- Use dynamic imports for large components/pages
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Write tests for critical business logic
|
||||||
|
- Test components in isolation
|
||||||
|
- Implement end-to-end tests for critical user flows
|
||||||
|
|
||||||
|
## Git Workflow
|
||||||
|
|
||||||
|
### Branch Naming
|
||||||
|
- Features: `feature/short-description`
|
||||||
|
- Bugfixes: `fix/short-description`
|
||||||
|
- Hotfixes: `hotfix/short-description`
|
||||||
|
- Releases: `release/version`
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
- Variable and function names in English
|
||||||
|
- Log and error messages in English
|
||||||
|
- Documentation in English
|
||||||
|
- User-facing content (emails, responses) in English
|
||||||
|
- Indentation with 4 spaces
|
||||||
|
- Maximum of 79 characters per line
|
||||||
|
|
||||||
|
## Commit Rules
|
||||||
|
- Use Conventional Commits format for all commit messages
|
||||||
|
- Format: `<type>(<scope>): <description>`
|
||||||
|
- Types:
|
||||||
|
- `feat`: A new feature
|
||||||
|
- `fix`: A bug fix
|
||||||
|
- `docs`: Documentation changes
|
||||||
|
- `style`: Changes that do not affect code meaning (formatting, etc.)
|
||||||
|
- `refactor`: Code changes that neither fix a bug nor add a feature
|
||||||
|
- `perf`: Performance improvements
|
||||||
|
- `test`: Adding or modifying tests
|
||||||
|
- `chore`: Changes to build process or auxiliary tools
|
||||||
|
- Scope is optional and should be the module or component affected
|
||||||
|
- Description should be concise, in the imperative mood, and not capitalized
|
||||||
|
- Use body for more detailed explanations if needed
|
||||||
|
- Reference issues in the footer with `Fixes #123` or `Relates to #123`
|
||||||
|
- Examples:
|
||||||
|
- `feat(auth): add password reset functionality`
|
||||||
|
- `fix(api): correct validation error in client registration`
|
||||||
|
- `docs: update API documentation for new endpoints`
|
||||||
|
- `refactor(services): improve error handling in authentication`
|
||||||
|
|
||||||
|
Format: `type(scope): subject`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `feat(auth): add login form validation`
|
||||||
|
- `fix(api): resolve user data fetching issue`
|
||||||
|
- `docs(readme): update installation instructions`
|
||||||
|
- `style(components): format according to style guide`
|
||||||
|
|
||||||
|
### Pull Requests
|
||||||
|
- Keep PRs focused on a single feature or fix
|
||||||
|
- Include descriptive titles and descriptions
|
||||||
|
- Reference related issues
|
||||||
|
- Request code reviews from appropriate team members
|
||||||
|
- Ensure CI checks pass before merging
|
||||||
|
|
||||||
|
## Code Review Guidelines
|
||||||
|
- Focus on code quality, architecture, and maintainability
|
||||||
|
- Provide constructive feedback
|
||||||
|
- Address all review comments before merging
|
||||||
|
- Maintain a respectful and collaborative tone
|
||||||
61
frontend/.dockerignore
Normal file
61
frontend/.dockerignore
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Production builds
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile*
|
||||||
|
docker-compose*
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage/
|
||||||
|
.nyc_output
|
||||||
|
.coverage
|
||||||
|
|
||||||
|
# Other
|
||||||
|
.cache/
|
||||||
1
frontend/.env.example
Normal file
1
frontend/.env.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||||
31
frontend/.gitignore
vendored
Normal file
31
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Lock files
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
|
# env files
|
||||||
|
.env
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
67
frontend/CHANGELOG.md
Normal file
67
frontend/CHANGELOG.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.0.7] - 2025-05-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add Task agents
|
||||||
|
- Add file support for A2A protocol (Agent-to-Agent) endpoints
|
||||||
|
- Add entrypoint script for dynamic environment variable handling
|
||||||
|
- Add agent card URL input and copy functionality
|
||||||
|
|
||||||
|
## [0.0.6] - 2025-05-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Agent sharing functionality with third parties via API keys
|
||||||
|
- Dedicated shared-chat page for accessing shared agents
|
||||||
|
- Local storage mechanism to save recently used shared agents
|
||||||
|
- Public access to shared agents without full authentication
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Add example environment file and update .gitignore
|
||||||
|
- Add clientId prop to agent-related components and improve agent data processing
|
||||||
|
- Refactor middleware to handle shared agent routes as public paths
|
||||||
|
- Update API interceptors to prevent forced logout on shared chat pages
|
||||||
|
|
||||||
|
### security
|
||||||
|
|
||||||
|
- Implement force logout functionality on 401 Unauthorized responses
|
||||||
|
|
||||||
|
## [0.0.5] - 2025-05-13
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Update author information in multiple files
|
||||||
|
|
||||||
|
## [0.0.4] - 2025-05-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial public release
|
||||||
|
- User-friendly interface for creating and managing AI agents
|
||||||
|
- Integration with multiple language models (e.g., GPT-4, Claude)
|
||||||
|
- Client management interface
|
||||||
|
- Visual configuration for MCP servers
|
||||||
|
- Custom tools management
|
||||||
|
- JWT authentication with email verification
|
||||||
|
- Agent 2 Agent (A2A) protocol support (Google's A2A spec)
|
||||||
|
- Workflow Agent with ReactFlow for visual workflow creation
|
||||||
|
- Secure API key management (encrypted storage)
|
||||||
|
- Agent organization with folders and categories
|
||||||
|
- Dashboard with agent overview, usage stats, and recent activities
|
||||||
|
- Agent editor for creating, editing, and configuring agents
|
||||||
|
- Workflow editor for building and visualizing agent flows
|
||||||
|
- API key manager for adding, encrypting, and rotating keys
|
||||||
|
- RESTful API and WebSocket backend integration
|
||||||
|
- Docker support for containerized deployment
|
||||||
|
- Complete documentation and contribution guidelines
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Older versions and future releases will be listed here.
|
||||||
60
frontend/Dockerfile
Normal file
60
frontend/Dockerfile
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:20.15.1-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Define build arguments with default values
|
||||||
|
ARG NEXT_PUBLIC_API_URL=https://api.evo-ai.co
|
||||||
|
|
||||||
|
# Install pnpm globally
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
|
# Copy package files first for better caching
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN pnpm install --no-frozen-lockfile
|
||||||
|
|
||||||
|
# Copy all source code (this includes tsconfig.json, lib/, etc.)
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Set environment variables from build arguments
|
||||||
|
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:20.15.1-alpine AS runner
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Define build arguments again for the runner stage
|
||||||
|
ARG NEXT_PUBLIC_API_URL=https://api-evoai.evoapicloud.com
|
||||||
|
|
||||||
|
# Install pnpm globally
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
|
# Install production dependencies only
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
RUN pnpm install --prod --no-frozen-lockfile
|
||||||
|
|
||||||
|
# Copy built assets from builder
|
||||||
|
COPY --from=builder /app/.next ./.next
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder /app/next.config.mjs ./
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||||
|
|
||||||
|
# Script to replace environment variables at runtime
|
||||||
|
COPY docker-entrypoint.sh ./
|
||||||
|
RUN chmod +x ./docker-entrypoint.sh
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Use entrypoint script to initialize environment variables before starting the app
|
||||||
|
ENTRYPOINT ["sh", "./docker-entrypoint.sh"]
|
||||||
201
frontend/LICENSE
Normal file
201
frontend/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2025 Evolution API
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
237
frontend/README.md
Normal file
237
frontend/README.md
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
# Evo AI - AI Agents Platform (Frontend)
|
||||||
|
|
||||||
|
Evo AI is an open-source platform for creating and managing AI agents, enabling integration with different AI models and services.
|
||||||
|
|
||||||
|
## 🚀 Overview
|
||||||
|
|
||||||
|
The Evo AI frontend platform enables:
|
||||||
|
|
||||||
|
- User-friendly interface for creating and managing AI agents
|
||||||
|
- Integration with different language models
|
||||||
|
- Client management
|
||||||
|
- Visual configuration of MCP servers
|
||||||
|
- Custom tools management
|
||||||
|
- JWT authentication with email verification
|
||||||
|
- **Agent 2 Agent (A2A) Protocol Support**: Interface for interoperability between AI agents following Google's A2A specification
|
||||||
|
- **Workflow Agent with ReactFlow**: Visual interface for building complex agent workflows
|
||||||
|
- **Secure API Key Management**: Interface for encrypted storage of API keys
|
||||||
|
- **Agent Organization**: Folder structure for organizing agents by categories
|
||||||
|
|
||||||
|
## 🧩 Agent Creation Interface
|
||||||
|
|
||||||
|
The frontend offers intuitive interfaces for creating different types of agents:
|
||||||
|
|
||||||
|
### 1. LLM Agent (Language Model)
|
||||||
|
|
||||||
|
Interface for configuring agents based on models like GPT-4, Claude, etc. with tools, MCP servers, and sub-agents.
|
||||||
|
|
||||||
|
### 2. A2A Agent (Agent-to-Agent)
|
||||||
|
|
||||||
|
Interface for implementing Google's A2A protocol for agent interoperability.
|
||||||
|
|
||||||
|
### 3. Sequential Agent
|
||||||
|
|
||||||
|
Interface for executing sub-agents in a specific order.
|
||||||
|
|
||||||
|
### 4. Parallel Agent
|
||||||
|
|
||||||
|
Interface for executing multiple sub-agents simultaneously.
|
||||||
|
|
||||||
|
### 5. Loop Agent
|
||||||
|
|
||||||
|
Interface for executing sub-agents in a loop with a defined number of iterations.
|
||||||
|
|
||||||
|
### 6. Workflow Agent
|
||||||
|
|
||||||
|
Visual interface based on ReactFlow for creating complex workflows between agents.
|
||||||
|
|
||||||
|
## 🛠️ Technologies
|
||||||
|
|
||||||
|
- [Next.js](https://nextjs.org/) - React framework for production
|
||||||
|
- [React](https://reactjs.org/) - JavaScript library for building user interfaces
|
||||||
|
- [Tailwind CSS](https://tailwindcss.com/) - Utility-first CSS framework
|
||||||
|
- [Shadcn UI](https://ui.shadcn.com/) - UI component library
|
||||||
|
- [Radix UI](https://www.radix-ui.com/) - Unstyled, accessible components
|
||||||
|
- [TypeScript](https://www.typescriptlang.org/) - Typed JavaScript
|
||||||
|
- [React Query](https://tanstack.com/query/latest) - Data fetching and state management
|
||||||
|
- [Zustand](https://zustand-demo.pmnd.rs/) - Global state management
|
||||||
|
- [React Flow](https://reactflow.dev/) - Library for building node-based visual workflows
|
||||||
|
- [Axios](https://axios-http.com/) - HTTP client for API communication
|
||||||
|
|
||||||
|
## 📋 Requirements
|
||||||
|
|
||||||
|
- Node.js 18+ (LTS recommended)
|
||||||
|
- npm, yarn, or pnpm package manager
|
||||||
|
- Evo AI backend running
|
||||||
|
|
||||||
|
## 🔧 Installation
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/EvolutionAPI/evo-ai-frontend.git
|
||||||
|
cd evo-ai-frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
# or
|
||||||
|
yarn install
|
||||||
|
# or
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Configure environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit the .env file with your settings
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Running the Project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development mode
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# Production build
|
||||||
|
npm run build
|
||||||
|
# or
|
||||||
|
yarn build
|
||||||
|
# or
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# Start production server
|
||||||
|
npm run start
|
||||||
|
# or
|
||||||
|
yarn start
|
||||||
|
# or
|
||||||
|
pnpm start
|
||||||
|
```
|
||||||
|
|
||||||
|
The project will be available at [http://localhost:3000](http://localhost:3000)
|
||||||
|
|
||||||
|
## 🔐 Authentication
|
||||||
|
|
||||||
|
The frontend implements JWT authentication integrated with the backend:
|
||||||
|
|
||||||
|
- **User Registration**: Form for creating new accounts
|
||||||
|
- **Email Verification**: Process for verifying via email
|
||||||
|
- **Login**: Authentication of existing users
|
||||||
|
- **Password Recovery**: Complete password recovery flow
|
||||||
|
- **Secure Storage**: Tokens stored in HttpOnly cookies
|
||||||
|
|
||||||
|
## 🖥️ Main Interface Features
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
|
||||||
|
Main dashboard showing:
|
||||||
|
- Agent overview
|
||||||
|
- Usage statistics
|
||||||
|
- Recent activities
|
||||||
|
- Quick links for agent creation
|
||||||
|
|
||||||
|
### Agent Editor
|
||||||
|
|
||||||
|
Complete interface for:
|
||||||
|
- Creating new agents
|
||||||
|
- Editing existing agents
|
||||||
|
- Configuring instructions
|
||||||
|
- Selecting models
|
||||||
|
- Setting up API keys
|
||||||
|
|
||||||
|
### Workflow Editor
|
||||||
|
|
||||||
|
Visual editor based on ReactFlow for:
|
||||||
|
- Creating complex workflows
|
||||||
|
- Connecting different agents
|
||||||
|
- Defining conditionals and decision flows
|
||||||
|
- Visualizing data flow
|
||||||
|
|
||||||
|
### API Key Manager
|
||||||
|
|
||||||
|
Interface for:
|
||||||
|
- Adding new API keys
|
||||||
|
- Securely encrypting keys
|
||||||
|
- Managing existing keys
|
||||||
|
- Rotating and updating keys
|
||||||
|
|
||||||
|
### Agent Organization
|
||||||
|
|
||||||
|
System for:
|
||||||
|
- Creating folders and categories
|
||||||
|
- Organizing agents by type or use case
|
||||||
|
- Searching and filtering agents
|
||||||
|
|
||||||
|
## 🔄 Backend Integration
|
||||||
|
|
||||||
|
The frontend communicates with the backend through:
|
||||||
|
|
||||||
|
- **RESTful API**: Endpoints for resource management
|
||||||
|
- **WebSockets**: Real-time communication for agent messages
|
||||||
|
- **Response Streaming**: Support for streaming model responses
|
||||||
|
|
||||||
|
## 🐳 Docker Support
|
||||||
|
|
||||||
|
The project includes Docker configuration for containerized deployment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the Docker image
|
||||||
|
./docker_build.sh
|
||||||
|
# or
|
||||||
|
docker build -t nextjs-frontend .
|
||||||
|
|
||||||
|
# Run the container
|
||||||
|
docker run -p 3000:3000 nextjs-frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
# 🐳 Docker Compose
|
||||||
|
```bash
|
||||||
|
# Copy the .env file
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Build and deploy
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
We welcome contributions from the community! Here's how you can help:
|
||||||
|
|
||||||
|
1. Fork the project
|
||||||
|
2. Create a feature branch (`git checkout -b feature/AmazingFeature`)
|
||||||
|
3. Make your changes and add tests if possible
|
||||||
|
4. Run tests and make sure they pass
|
||||||
|
5. Commit your changes following conventional commits format (`feat: add amazing feature`)
|
||||||
|
6. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||||
|
7. Open a Pull Request
|
||||||
|
|
||||||
|
Please read our [Contributing Guidelines](CONTRIBUTING.md) for more details.
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
**Trademark Notice:** The name "Evo AI" and related branding are protected trademarks. Unauthorized use is prohibited.
|
||||||
|
|
||||||
|
## 👨💻 Development Commands
|
||||||
|
|
||||||
|
- `npm run dev` - Start the development server
|
||||||
|
- `npm run build` - Build the application for production
|
||||||
|
- `npm run start` - Start the production server
|
||||||
|
- `npm run lint` - Run ESLint to check code quality
|
||||||
|
- `npm run format` - Format code with Prettier
|
||||||
|
|
||||||
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
|
- [Next.js](https://nextjs.org/)
|
||||||
|
- [React](https://reactjs.org/)
|
||||||
|
- [Tailwind CSS](https://tailwindcss.com/)
|
||||||
|
- [Shadcn UI](https://ui.shadcn.com/)
|
||||||
|
- [ReactFlow](https://reactflow.dev/)
|
||||||
506
frontend/app/agents/AgentCard.tsx
Normal file
506
frontend/app/agents/AgentCard.tsx
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/AgentCard.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Folder } from "@/services/agentService";
|
||||||
|
import { Agent, AgentType } from "@/types/agent";
|
||||||
|
import { MCPServer } from "@/types/mcpServer";
|
||||||
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
Bot,
|
||||||
|
BookOpenCheck,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
Code,
|
||||||
|
ExternalLink,
|
||||||
|
GitBranch,
|
||||||
|
MoveRight,
|
||||||
|
Pencil,
|
||||||
|
RefreshCw,
|
||||||
|
Settings,
|
||||||
|
Share2,
|
||||||
|
Trash2,
|
||||||
|
Workflow,
|
||||||
|
TextSelect,
|
||||||
|
Download,
|
||||||
|
FlaskConical,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { exportAsJson } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface AgentCardProps {
|
||||||
|
agent: Agent;
|
||||||
|
onEdit: (agent: Agent) => void;
|
||||||
|
onDelete: (agent: Agent) => void;
|
||||||
|
onMove: (agent: Agent) => void;
|
||||||
|
onShare?: (agent: Agent) => void;
|
||||||
|
onWorkflow?: (agentId: string) => void;
|
||||||
|
availableMCPs?: MCPServer[];
|
||||||
|
getApiKeyNameById?: (id: string | undefined) => string | null;
|
||||||
|
getAgentNameById?: (id: string) => string;
|
||||||
|
folders?: Folder[];
|
||||||
|
agents: Agent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgentCard({
|
||||||
|
agent,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
onMove,
|
||||||
|
onShare,
|
||||||
|
onWorkflow,
|
||||||
|
availableMCPs = [],
|
||||||
|
getApiKeyNameById = () => null,
|
||||||
|
getAgentNameById = (id) => id,
|
||||||
|
folders = [],
|
||||||
|
agents,
|
||||||
|
}: AgentCardProps) {
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const getAgentTypeInfo = (type: AgentType) => {
|
||||||
|
const types: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
label: string;
|
||||||
|
icon: React.ElementType;
|
||||||
|
color: string;
|
||||||
|
bgColor: string;
|
||||||
|
badgeClass: string;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
llm: {
|
||||||
|
label: "LLM Agent",
|
||||||
|
icon: Code,
|
||||||
|
color: "#00cc7d",
|
||||||
|
bgColor: "bg-green-500/10",
|
||||||
|
badgeClass:
|
||||||
|
"bg-green-900/30 text-green-400 border-green-600/30 hover:bg-green-900/40",
|
||||||
|
},
|
||||||
|
a2a: {
|
||||||
|
label: "A2A Agent",
|
||||||
|
icon: ExternalLink,
|
||||||
|
color: "#6366f1",
|
||||||
|
bgColor: "bg-indigo-500/10",
|
||||||
|
badgeClass:
|
||||||
|
"bg-indigo-900/30 text-indigo-400 border-indigo-600/30 hover:bg-indigo-900/40",
|
||||||
|
},
|
||||||
|
sequential: {
|
||||||
|
label: "Sequential Agent",
|
||||||
|
icon: ArrowRight,
|
||||||
|
color: "#f59e0b",
|
||||||
|
bgColor: "bg-yellow-500/10",
|
||||||
|
badgeClass:
|
||||||
|
"bg-yellow-900/30 text-yellow-400 border-yellow-600/30 hover:bg-yellow-900/40",
|
||||||
|
},
|
||||||
|
parallel: {
|
||||||
|
label: "Parallel Agent",
|
||||||
|
icon: GitBranch,
|
||||||
|
color: "#8b5cf6",
|
||||||
|
bgColor: "bg-purple-500/10",
|
||||||
|
badgeClass:
|
||||||
|
"bg-purple-900/30 text-purple-400 border-purple-600/30 hover:bg-purple-900/40",
|
||||||
|
},
|
||||||
|
loop: {
|
||||||
|
label: "Loop Agent",
|
||||||
|
icon: RefreshCw,
|
||||||
|
color: "#ec4899",
|
||||||
|
bgColor: "bg-pink-500/10",
|
||||||
|
badgeClass:
|
||||||
|
"bg-orange-900/30 text-orange-400 border-orange-600/30 hover:bg-orange-900/40",
|
||||||
|
},
|
||||||
|
workflow: {
|
||||||
|
label: "Workflow Agent",
|
||||||
|
icon: Workflow,
|
||||||
|
color: "#3b82f6",
|
||||||
|
bgColor: "bg-blue-500/10",
|
||||||
|
badgeClass:
|
||||||
|
"bg-blue-900/30 text-blue-400 border-blue-700/40 hover:bg-blue-900/40",
|
||||||
|
},
|
||||||
|
task: {
|
||||||
|
label: "Task Agent",
|
||||||
|
icon: BookOpenCheck,
|
||||||
|
color: "#ef4444",
|
||||||
|
bgColor: "bg-red-500/10",
|
||||||
|
badgeClass:
|
||||||
|
"bg-red-900/30 text-red-400 border-red-600/30 hover:bg-red-900/40",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
types[type] || {
|
||||||
|
label: type,
|
||||||
|
icon: Bot,
|
||||||
|
color: "#94a3b8",
|
||||||
|
bgColor: "bg-slate-500/10",
|
||||||
|
badgeClass:
|
||||||
|
"bg-slate-900/30 text-slate-400 border-slate-600/30 hover:bg-slate-900/40",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAgentTypeIcon = (type: AgentType) => {
|
||||||
|
const typeInfo = getAgentTypeInfo(type);
|
||||||
|
const IconComponent = typeInfo.icon;
|
||||||
|
return (
|
||||||
|
<IconComponent className="h-5 w-5" style={{ color: typeInfo.color }} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAgentTypeName = (type: AgentType) => {
|
||||||
|
return getAgentTypeInfo(type).label;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAgentTypeBgColor = (type: AgentType) => {
|
||||||
|
return getAgentTypeInfo(type).bgColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAgentTypeBadgeClass = (type: AgentType) => {
|
||||||
|
return getAgentTypeInfo(type).badgeClass;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFolderNameById = (id: string) => {
|
||||||
|
const folder = folders?.find((f) => f.id === id);
|
||||||
|
return folder?.name || id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTotalTools = () => {
|
||||||
|
if (agent.type === "llm" && agent.config?.mcp_servers) {
|
||||||
|
return agent.config.mcp_servers.reduce(
|
||||||
|
(total, mcp) => total + (mcp.tools?.length || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCreatedAtFormatted = () => {
|
||||||
|
return new Date(agent.created_at).toLocaleDateString();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to export the agent as JSON
|
||||||
|
const handleExportAgent = () => {
|
||||||
|
try {
|
||||||
|
exportAsJson(
|
||||||
|
agent,
|
||||||
|
`agent-${agent.name
|
||||||
|
.replace(/\s+/g, "-")
|
||||||
|
.toLowerCase()}-${agent.id.substring(0, 8)}`,
|
||||||
|
true,
|
||||||
|
agents
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error exporting agent:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to test the A2A agent in the lab
|
||||||
|
const handleTestA2A = () => {
|
||||||
|
// Use the agent card URL as base for A2A tests
|
||||||
|
const agentUrl = agent.agent_card_url?.replace(
|
||||||
|
"/.well-known/agent.json",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use the API key directly from the agent config
|
||||||
|
const apiKey = agent.config?.api_key;
|
||||||
|
|
||||||
|
// Build the URL with parameters for the lab tests
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (agentUrl) {
|
||||||
|
params.set("agent_url", agentUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
params.set("api_key", apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to the lab tests in the "lab" tab
|
||||||
|
const testUrl = `/documentation?${params.toString()}#lab`;
|
||||||
|
|
||||||
|
router.push(testUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full overflow-hidden border border-zinc-800 shadow-lg bg-gradient-to-br from-zinc-800 to-zinc-900">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"p-4 flex justify-between items-center border-b border-zinc-800",
|
||||||
|
getAgentTypeBgColor(agent.type)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getAgentTypeIcon(agent.type)}
|
||||||
|
<h3 className="font-medium text-white">{agent.name}</h3>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={cn("border", getAgentTypeBadgeClass(agent.type))}
|
||||||
|
>
|
||||||
|
{getAgentTypeName(agent.type)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="p-4 border-b border-zinc-800">
|
||||||
|
<p className="text-sm text-zinc-300">
|
||||||
|
{agent.description && agent.description.length > 100
|
||||||
|
? `${agent.description.substring(0, 100)}...`
|
||||||
|
: agent.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"p-4 flex justify-between items-center",
|
||||||
|
getAgentTypeBgColor(agent.type),
|
||||||
|
"bg-opacity-20"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-zinc-500">Model:</span>
|
||||||
|
<span className="text-xs font-medium text-zinc-300">
|
||||||
|
{agent.type === "llm" ? agent.model : "N/A"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={cn("p-0 h-auto", {
|
||||||
|
"text-green-400 hover:text-green-300": agent.type === "llm",
|
||||||
|
"text-indigo-400 hover:text-indigo-300": agent.type === "a2a",
|
||||||
|
"text-yellow-400 hover:text-yellow-300":
|
||||||
|
agent.type === "sequential",
|
||||||
|
"text-purple-400 hover:text-purple-300":
|
||||||
|
agent.type === "parallel",
|
||||||
|
"text-orange-400 hover:text-orange-300": agent.type === "loop",
|
||||||
|
"text-blue-400 hover:text-blue-300": agent.type === "workflow",
|
||||||
|
"text-red-400 hover:text-red-300": agent.type === "task",
|
||||||
|
"text-zinc-400 hover:text-white": ![
|
||||||
|
"llm",
|
||||||
|
"a2a",
|
||||||
|
"sequential",
|
||||||
|
"parallel",
|
||||||
|
"loop",
|
||||||
|
"workflow",
|
||||||
|
"task",
|
||||||
|
].includes(agent.type),
|
||||||
|
})}
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
>
|
||||||
|
{expanded ? (
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
<span className="ml-1 text-xs">{expanded ? "Less" : "More"}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{expanded && (
|
||||||
|
<div className="p-4 bg-zinc-950 text-xs space-y-3 animate-in fade-in-50 duration-200">
|
||||||
|
{agent.folder_id && (
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-zinc-500">Folder:</span>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"h-5 px-2 bg-transparent",
|
||||||
|
getAgentTypeBadgeClass(agent.type)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{getFolderNameById(agent.folder_id)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{agent.type === "llm" && agent.api_key_id && (
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-zinc-500">API Key:</span>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"h-5 px-2 bg-transparent",
|
||||||
|
getAgentTypeBadgeClass(agent.type)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{getApiKeyNameById(agent.api_key_id)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{getTotalTools() > 0 && (
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-zinc-500">Tools:</span>
|
||||||
|
<span className="text-zinc-300">{getTotalTools()}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{agent.config?.sub_agents && agent.config.sub_agents.length > 0 && (
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-zinc-500">Sub-agents:</span>
|
||||||
|
<span className="text-zinc-300">
|
||||||
|
{agent.config.sub_agents.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{agent.type === "workflow" && agent.config?.workflow && (
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-zinc-500">Elements:</span>
|
||||||
|
<span className="text-zinc-300">
|
||||||
|
{agent.config.workflow.nodes?.length || 0} nodes,{" "}
|
||||||
|
{agent.config.workflow.edges?.length || 0} connections
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-zinc-500">Created at:</span>
|
||||||
|
<span className="text-zinc-300">{getCreatedAtFormatted()}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-zinc-500">ID:</span>
|
||||||
|
<span className="text-zinc-300 text-[10px]">{agent.id}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex border-t border-zinc-800">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="flex-1 rounded-none h-12 text-zinc-400 hover:text-white hover:bg-zinc-800 focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none"
|
||||||
|
>
|
||||||
|
<Settings className="h-4 w-4 mr-2" />
|
||||||
|
Configure
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
className="bg-zinc-900 border-zinc-700"
|
||||||
|
side="bottom"
|
||||||
|
align="end"
|
||||||
|
>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-white hover:bg-zinc-800 cursor-pointer"
|
||||||
|
onClick={handleTestA2A}
|
||||||
|
>
|
||||||
|
<FlaskConical className="h-4 w-4 mr-2 text-emerald-400" />
|
||||||
|
Test A2A
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-white hover:bg-zinc-800 cursor-pointer"
|
||||||
|
onClick={() => onEdit(agent)}
|
||||||
|
>
|
||||||
|
<Pencil className="h-4 w-4 mr-2 text-emerald-400" />
|
||||||
|
Edit Agent
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-white hover:bg-zinc-800 cursor-pointer"
|
||||||
|
onClick={() => onMove(agent)}
|
||||||
|
>
|
||||||
|
<MoveRight className="h-4 w-4 mr-2 text-yellow-400" />
|
||||||
|
Move Agent
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{onWorkflow && agent.type === "workflow" && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-white hover:bg-zinc-800 cursor-pointer"
|
||||||
|
onClick={() => onWorkflow(agent.id)}
|
||||||
|
>
|
||||||
|
<Workflow className="h-4 w-4 mr-2 text-blue-400" />
|
||||||
|
Open Workflow
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-white hover:bg-zinc-800 cursor-pointer"
|
||||||
|
onClick={handleExportAgent}
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4 mr-2 text-purple-400" />
|
||||||
|
Export as JSON
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{onShare && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-white hover:bg-zinc-800 cursor-pointer"
|
||||||
|
onClick={() => onShare(agent)}
|
||||||
|
>
|
||||||
|
<Share2 className="h-4 w-4 mr-2 text-blue-400" />
|
||||||
|
Share Agent
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-red-500 hover:bg-zinc-800 cursor-pointer"
|
||||||
|
onClick={() => onDelete(agent)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
Delete Agent
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<div className="w-px bg-zinc-800" />
|
||||||
|
<a
|
||||||
|
href={agent.agent_card_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={cn(
|
||||||
|
"flex-1 flex items-center justify-center rounded-none h-12 hover:bg-zinc-800",
|
||||||
|
{
|
||||||
|
"text-green-400 hover:text-green-300": agent.type === "llm",
|
||||||
|
"text-indigo-400 hover:text-indigo-300": agent.type === "a2a",
|
||||||
|
"text-yellow-400 hover:text-yellow-300":
|
||||||
|
agent.type === "sequential",
|
||||||
|
"text-purple-400 hover:text-purple-300":
|
||||||
|
agent.type === "parallel",
|
||||||
|
"text-orange-400 hover:text-orange-300": agent.type === "loop",
|
||||||
|
"text-blue-400 hover:text-blue-300": agent.type === "workflow",
|
||||||
|
"text-red-400 hover:text-red-300": agent.type === "task",
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-4 w-4 mr-2" />
|
||||||
|
Agent Card
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
130
frontend/app/agents/AgentList.tsx
Normal file
130
frontend/app/agents/AgentList.tsx
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/AgentList.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Agent } from "@/types/agent";
|
||||||
|
import { MCPServer } from "@/types/mcpServer";
|
||||||
|
import { AgentCard } from "./AgentCard";
|
||||||
|
import { EmptyState } from "./EmptyState";
|
||||||
|
import { ApiKey, Folder } from "@/services/agentService";
|
||||||
|
|
||||||
|
interface AgentListProps {
|
||||||
|
agents: Agent[];
|
||||||
|
isLoading: boolean;
|
||||||
|
searchTerm: string;
|
||||||
|
selectedFolderId: string | null;
|
||||||
|
availableMCPs: MCPServer[];
|
||||||
|
getApiKeyNameById: (id: string | undefined) => string | null;
|
||||||
|
getAgentNameById: (id: string) => string;
|
||||||
|
onEdit: (agent: Agent) => void;
|
||||||
|
onDelete: (agent: Agent) => void;
|
||||||
|
onMove: (agent: Agent) => void;
|
||||||
|
onShare?: (agent: Agent) => void;
|
||||||
|
onWorkflow?: (agentId: string) => void;
|
||||||
|
onClearSearch?: () => void;
|
||||||
|
onCreateAgent?: () => void;
|
||||||
|
apiKeys: ApiKey[];
|
||||||
|
folders: Folder[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgentList({
|
||||||
|
agents,
|
||||||
|
isLoading,
|
||||||
|
searchTerm,
|
||||||
|
selectedFolderId,
|
||||||
|
availableMCPs,
|
||||||
|
getApiKeyNameById,
|
||||||
|
getAgentNameById,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
onMove,
|
||||||
|
onShare,
|
||||||
|
onWorkflow,
|
||||||
|
onClearSearch,
|
||||||
|
onCreateAgent,
|
||||||
|
apiKeys,
|
||||||
|
folders,
|
||||||
|
}: AgentListProps) {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-[60vh]">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-emerald-400"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agents.length === 0) {
|
||||||
|
if (searchTerm) {
|
||||||
|
return (
|
||||||
|
<EmptyState
|
||||||
|
type="search-no-results"
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
onAction={onClearSearch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (selectedFolderId) {
|
||||||
|
return (
|
||||||
|
<EmptyState
|
||||||
|
type="empty-folder"
|
||||||
|
onAction={onCreateAgent}
|
||||||
|
actionLabel="Create Agent"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<EmptyState
|
||||||
|
type="no-agents"
|
||||||
|
onAction={onCreateAgent}
|
||||||
|
actionLabel="Create Agent"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{agents.map((agent) => (
|
||||||
|
<AgentCard
|
||||||
|
key={agent.id}
|
||||||
|
agent={agent}
|
||||||
|
onEdit={onEdit}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onMove={onMove}
|
||||||
|
onShare={onShare}
|
||||||
|
onWorkflow={onWorkflow}
|
||||||
|
availableMCPs={availableMCPs}
|
||||||
|
getApiKeyNameById={getApiKeyNameById}
|
||||||
|
getAgentNameById={getAgentNameById}
|
||||||
|
folders={folders}
|
||||||
|
agents={agents}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
186
frontend/app/agents/AgentSidebar.tsx
Normal file
186
frontend/app/agents/AgentSidebar.tsx
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/AgentSidebar.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Folder,
|
||||||
|
FolderPlus,
|
||||||
|
Home,
|
||||||
|
X,
|
||||||
|
CircleEllipsis,
|
||||||
|
Edit,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
interface AgentFolder {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AgentSidebarProps {
|
||||||
|
visible: boolean;
|
||||||
|
folders: AgentFolder[];
|
||||||
|
selectedFolderId: string | null;
|
||||||
|
onSelectFolder: (id: string | null) => void;
|
||||||
|
onAddFolder: () => void;
|
||||||
|
onEditFolder: (folder: AgentFolder) => void;
|
||||||
|
onDeleteFolder: (folder: AgentFolder) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgentSidebar({
|
||||||
|
visible,
|
||||||
|
folders,
|
||||||
|
selectedFolderId,
|
||||||
|
onSelectFolder,
|
||||||
|
onAddFolder,
|
||||||
|
onEditFolder,
|
||||||
|
onDeleteFolder,
|
||||||
|
onClose,
|
||||||
|
}: AgentSidebarProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{visible && (
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute right-4 top-4 z-40 bg-[#222] p-2 rounded-md text-emerald-400 hover:bg-[#333] hover:text-emerald-400 shadow-md transition-all"
|
||||||
|
aria-label="Hide folders"
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`fixed top-0 z-30 h-full w-64 bg-[#1a1a1a] p-4 shadow-xl transition-all duration-300 ease-in-out ${
|
||||||
|
visible ? "left-64 translate-x-0" : "left-0 -translate-x-full pointer-events-none"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h2 className="text-xl font-semibold text-white flex items-center">
|
||||||
|
<Folder className="h-5 w-5 mr-2 text-emerald-400" />
|
||||||
|
Folders
|
||||||
|
</h2>
|
||||||
|
<div className="flex space-x-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0 text-neutral-400 hover:text-emerald-400 hover:bg-[#222]"
|
||||||
|
onClick={onAddFolder}
|
||||||
|
>
|
||||||
|
<FolderPlus className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0 text-neutral-400 hover:text-emerald-400 hover:bg-[#222]"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<button
|
||||||
|
className={`w-full text-left px-3 py-2 rounded-md flex items-center ${
|
||||||
|
selectedFolderId === null
|
||||||
|
? "bg-[#333] text-emerald-400"
|
||||||
|
: "text-neutral-300 hover:bg-[#222] hover:text-white"
|
||||||
|
}`}
|
||||||
|
onClick={() => onSelectFolder(null)}
|
||||||
|
>
|
||||||
|
<Home className="h-4 w-4 mr-2" />
|
||||||
|
<span>All agents</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{folders.map((folder) => (
|
||||||
|
<div key={folder.id} className="flex items-center group">
|
||||||
|
<button
|
||||||
|
className={`flex-1 text-left px-3 py-2 rounded-md flex items-center ${
|
||||||
|
selectedFolderId === folder.id
|
||||||
|
? "bg-[#333] text-emerald-400"
|
||||||
|
: "text-neutral-300 hover:bg-[#222] hover:text-white"
|
||||||
|
}`}
|
||||||
|
onClick={() => onSelectFolder(folder.id)}
|
||||||
|
>
|
||||||
|
<Folder className="h-4 w-4 mr-2" />
|
||||||
|
<span className="truncate">{folder.name}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7 p-0 text-neutral-400 hover:text-white hover:bg-[#222]"
|
||||||
|
>
|
||||||
|
<CircleEllipsis className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="end"
|
||||||
|
className="bg-[#222] border-[#333] text-white"
|
||||||
|
>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer hover:bg-[#333] focus:bg-[#333]"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onEditFolder(folder);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4 mr-2" />
|
||||||
|
Edit
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer text-red-500 hover:bg-[#333] hover:text-red-400 focus:bg-[#333]"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDeleteFolder(folder);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
96
frontend/app/agents/AgentTypeSelector.tsx
Normal file
96
frontend/app/agents/AgentTypeSelector.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/AgentTypeSelector.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { AgentType } from "@/types/agent";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Code,
|
||||||
|
ExternalLink,
|
||||||
|
GitBranch,
|
||||||
|
RefreshCw,
|
||||||
|
Workflow,
|
||||||
|
Users,
|
||||||
|
BookOpenCheck,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
interface AgentTypeSelectorProps {
|
||||||
|
value: AgentType;
|
||||||
|
onValueChange: (value: AgentType) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const agentTypes = [
|
||||||
|
{ value: "llm", label: "LLM Agent", icon: Code },
|
||||||
|
{ value: "a2a", label: "A2A Agent", icon: ExternalLink },
|
||||||
|
{ value: "sequential", label: "Sequential Agent", icon: Workflow },
|
||||||
|
{ value: "parallel", label: "Parallel Agent", icon: GitBranch },
|
||||||
|
{ value: "loop", label: "Loop Agent", icon: RefreshCw },
|
||||||
|
{ value: "workflow", label: "Workflow Agent", icon: Workflow },
|
||||||
|
{ value: "task", label: "Task Agent", icon: BookOpenCheck },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function AgentTypeSelector({
|
||||||
|
value,
|
||||||
|
onValueChange,
|
||||||
|
className = "",
|
||||||
|
}: AgentTypeSelectorProps) {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={value}
|
||||||
|
onValueChange={(value: AgentType) => onValueChange(value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
className={`bg-[#222] border-[#444] text-white ${className}`}
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Select type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-[#222] border-[#444] text-white">
|
||||||
|
{agentTypes.map((type) => (
|
||||||
|
<SelectItem
|
||||||
|
key={type.value}
|
||||||
|
value={type.value}
|
||||||
|
className="data-[selected]:bg-[#333] data-[highlighted]:bg-[#333] text-white focus:!text-white hover:text-emerald-400 data-[selected]:!text-emerald-400"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<type.icon className="h-4 w-4 text-emerald-400" />
|
||||||
|
{type.label}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
107
frontend/app/agents/EmptyState.tsx
Normal file
107
frontend/app/agents/EmptyState.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/EmptyState.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Folder, Plus, Search, Server } from "lucide-react";
|
||||||
|
|
||||||
|
interface EmptyStateProps {
|
||||||
|
type: "no-agents" | "empty-folder" | "search-no-results";
|
||||||
|
searchTerm?: string;
|
||||||
|
onAction?: () => void;
|
||||||
|
actionLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmptyState({
|
||||||
|
type,
|
||||||
|
searchTerm = "",
|
||||||
|
onAction,
|
||||||
|
actionLabel = "Create Agent",
|
||||||
|
}: EmptyStateProps) {
|
||||||
|
const getIcon = () => {
|
||||||
|
switch (type) {
|
||||||
|
case "empty-folder":
|
||||||
|
return <Folder className="h-16 w-16 text-emerald-400" />;
|
||||||
|
case "search-no-results":
|
||||||
|
return <Search className="h-16 w-16 text-emerald-400" />;
|
||||||
|
case "no-agents":
|
||||||
|
default:
|
||||||
|
return <Server className="h-16 w-16 text-emerald-400" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTitle = () => {
|
||||||
|
switch (type) {
|
||||||
|
case "empty-folder":
|
||||||
|
return "Empty folder";
|
||||||
|
case "search-no-results":
|
||||||
|
return "No agents found";
|
||||||
|
case "no-agents":
|
||||||
|
default:
|
||||||
|
return "No agents found";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMessage = () => {
|
||||||
|
switch (type) {
|
||||||
|
case "empty-folder":
|
||||||
|
return "This folder is empty. Add agents or create a new one.";
|
||||||
|
case "search-no-results":
|
||||||
|
return `We couldn't find any agents that match your search: "${searchTerm}"`;
|
||||||
|
case "no-agents":
|
||||||
|
default:
|
||||||
|
return "You don't have any agents configured. Create your first agent to start!";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-[60vh] text-center">
|
||||||
|
<div className="mb-6 p-8 rounded-full bg-[#1a1a1a] border border-[#333]">
|
||||||
|
{getIcon()}
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-semibold text-white mb-3">{getTitle()}</h2>
|
||||||
|
<p className="text-neutral-300 mb-6 max-w-md">{getMessage()}</p>
|
||||||
|
{onAction && (
|
||||||
|
<Button
|
||||||
|
onClick={onAction}
|
||||||
|
className={
|
||||||
|
type === "search-no-results"
|
||||||
|
? "bg-[#222] text-white hover:bg-[#333]"
|
||||||
|
: "bg-emerald-400 text-black hover:bg-[#00cc7d] px-6 py-2 hover:shadow-[0_0_15px_rgba(0,255,157,0.2)]"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{type === "search-no-results" ? null : (
|
||||||
|
<Plus className="mr-2 h-5 w-5" />
|
||||||
|
)}
|
||||||
|
{type === "search-no-results" ? "Clear search" : actionLabel}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
153
frontend/app/agents/SearchInput.tsx
Normal file
153
frontend/app/agents/SearchInput.tsx
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/SearchInput.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Search, X, Filter } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
|
||||||
|
interface SearchInputProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
selectedAgentType?: string | null;
|
||||||
|
onAgentTypeChange?: (type: string | null) => void;
|
||||||
|
agentTypes?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using "all" as a special value to represent no filter
|
||||||
|
const ANY_TYPE_VALUE = "all";
|
||||||
|
|
||||||
|
export function SearchInput({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder = "Search agents...",
|
||||||
|
className = "",
|
||||||
|
selectedAgentType = null,
|
||||||
|
onAgentTypeChange,
|
||||||
|
agentTypes = [],
|
||||||
|
}: SearchInputProps) {
|
||||||
|
const [isFilterOpen, setIsFilterOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleTypeChange = (value: string) => {
|
||||||
|
if (onAgentTypeChange) {
|
||||||
|
onAgentTypeChange(value === ANY_TYPE_VALUE ? null : value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`relative flex items-center gap-2 ${className}`}>
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-neutral-400" />
|
||||||
|
<Input
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
className="pl-10 w-full bg-[#222] border-[#444] text-white focus:border-emerald-400 focus:ring-emerald-400/10"
|
||||||
|
/>
|
||||||
|
{value && (
|
||||||
|
<button
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white"
|
||||||
|
onClick={() => onChange("")}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{agentTypes.length > 0 && onAgentTypeChange && (
|
||||||
|
<Popover open={isFilterOpen} onOpenChange={setIsFilterOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={`px-3 py-2 bg-[#222] border-[#444] hover:bg-[#333] ${
|
||||||
|
selectedAgentType ? "text-emerald-400" : "text-neutral-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Filter className="h-4 w-4 mr-1" />
|
||||||
|
{selectedAgentType ? "Filtered" : "Filter"}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-48 p-2 bg-[#222] border-[#444]">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-xs font-medium text-neutral-300">
|
||||||
|
Filter by type
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={selectedAgentType ? selectedAgentType : ANY_TYPE_VALUE}
|
||||||
|
onValueChange={handleTypeChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="bg-[#333] border-[#444] text-white">
|
||||||
|
<SelectValue placeholder="Any type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-[#333] border-[#444] text-white">
|
||||||
|
<SelectItem value={ANY_TYPE_VALUE}>Any type</SelectItem>
|
||||||
|
{agentTypes.map((type) => (
|
||||||
|
<SelectItem key={type} value={type}>
|
||||||
|
{type}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{selectedAgentType && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="w-full text-xs text-neutral-300 hover:text-white"
|
||||||
|
onClick={() => {
|
||||||
|
onAgentTypeChange(null);
|
||||||
|
setIsFilterOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear filter
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
73
frontend/app/agents/config/A2AAgentConfig.tsx
Normal file
73
frontend/app/agents/config/A2AAgentConfig.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/config/A2AAgentConfig.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
|
interface A2AAgentConfigProps {
|
||||||
|
values: {
|
||||||
|
agent_card_url?: string;
|
||||||
|
};
|
||||||
|
onChange: (values: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function A2AAgentConfig({ values, onChange }: A2AAgentConfigProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="agent_card_url" className="text-right text-neutral-300">
|
||||||
|
Agent Card URL
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="agent_card_url"
|
||||||
|
value={values.agent_card_url || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
agent_card_url: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="https://example.com/.well-known/agent-card.json"
|
||||||
|
className="col-span-3 bg-[#222] border-[#444] text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="pl-[25%] text-sm text-neutral-400">
|
||||||
|
<p>
|
||||||
|
Provide the full URL for the JSON file of the Agent Card that describes
|
||||||
|
this agent.
|
||||||
|
</p>
|
||||||
|
<p className="mt-2">
|
||||||
|
Agent Cards contain metadata, capabilities descriptions and supported
|
||||||
|
protocols.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
367
frontend/app/agents/config/LLMAgentConfig.tsx
Normal file
367
frontend/app/agents/config/LLMAgentConfig.tsx
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/config/LLMAgentConfig.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { ApiKey } from "@/services/agentService";
|
||||||
|
import { Plus, Maximize2, Save } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
interface ModelOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
provider: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LLMAgentConfigProps {
|
||||||
|
apiKeys: ApiKey[];
|
||||||
|
availableModels: ModelOption[];
|
||||||
|
values: {
|
||||||
|
model?: string;
|
||||||
|
api_key_id?: string;
|
||||||
|
instruction?: string;
|
||||||
|
role?: string;
|
||||||
|
goal?: string;
|
||||||
|
};
|
||||||
|
onChange: (values: any) => void;
|
||||||
|
onOpenApiKeysDialog: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LLMAgentConfig({
|
||||||
|
apiKeys,
|
||||||
|
availableModels,
|
||||||
|
values,
|
||||||
|
onChange,
|
||||||
|
onOpenApiKeysDialog,
|
||||||
|
}: LLMAgentConfigProps) {
|
||||||
|
const [instructionText, setInstructionText] = useState(values.instruction || "");
|
||||||
|
const [isInstructionModalOpen, setIsInstructionModalOpen] = useState(false);
|
||||||
|
const [expandedInstructionText, setExpandedInstructionText] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInstructionText(values.instruction || "");
|
||||||
|
}, [values.instruction]);
|
||||||
|
|
||||||
|
const handleInstructionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
setInstructionText(newValue);
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
instruction: newValue,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExpandInstruction = () => {
|
||||||
|
setExpandedInstructionText(instructionText);
|
||||||
|
setIsInstructionModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveExpandedInstruction = () => {
|
||||||
|
setInstructionText(expandedInstructionText);
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
instruction: expandedInstructionText,
|
||||||
|
});
|
||||||
|
setIsInstructionModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="role" className="text-right text-neutral-300">
|
||||||
|
Role
|
||||||
|
</Label>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<Input
|
||||||
|
id="role"
|
||||||
|
value={values.role || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
role: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="Ex: Research Assistant, Customer Support, etc."
|
||||||
|
className="bg-[#222] border-[#444] text-white"
|
||||||
|
/>
|
||||||
|
<div className="mt-1 text-xs text-neutral-400">
|
||||||
|
<span className="inline-block h-3 w-3 mr-1">ℹ️</span>
|
||||||
|
<span>Define the role or persona that the agent will assume</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="goal" className="text-right text-neutral-300">
|
||||||
|
Goal
|
||||||
|
</Label>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<Input
|
||||||
|
id="goal"
|
||||||
|
value={values.goal || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
goal: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="Ex: Find and organize information, Assist customers with inquiries, etc."
|
||||||
|
className="bg-[#222] border-[#444] text-white"
|
||||||
|
/>
|
||||||
|
<div className="mt-1 text-xs text-neutral-400">
|
||||||
|
<span className="inline-block h-3 w-3 mr-1">ℹ️</span>
|
||||||
|
<span>Define the main objective or purpose of this agent</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="api_key" className="text-right text-neutral-300">
|
||||||
|
API Key
|
||||||
|
</Label>
|
||||||
|
<div className="col-span-3 space-y-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Select
|
||||||
|
value={values.api_key_id || ""}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
api_key_id: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="flex-1 bg-[#222] border-[#444] text-white">
|
||||||
|
<SelectValue placeholder="Select an API key" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-[#222] border-[#444] text-white">
|
||||||
|
{apiKeys.length > 0 ? (
|
||||||
|
apiKeys
|
||||||
|
.filter((key) => key.is_active !== false)
|
||||||
|
.map((key) => (
|
||||||
|
<SelectItem
|
||||||
|
key={key.id}
|
||||||
|
value={key.id}
|
||||||
|
className="data-[selected]:bg-[#333] data-[highlighted]:bg-[#333] !text-white focus:!text-white hover:text-emerald-400 data-[selected]:!text-emerald-400"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span>{key.name}</span>
|
||||||
|
<Badge className="ml-2 bg-[#333] text-emerald-400 text-xs">
|
||||||
|
{key.provider}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-neutral-500 px-2 py-1.5 pl-8">
|
||||||
|
No API keys available
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={onOpenApiKeysDialog}
|
||||||
|
className="ml-2 bg-[#222] text-emerald-400 hover:bg-[#333]"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{apiKeys.length === 0 && (
|
||||||
|
<div className="flex items-center text-xs text-neutral-400">
|
||||||
|
<span className="inline-block h-3 w-3 mr-1 text-neutral-400">i</span>
|
||||||
|
<span>
|
||||||
|
You need to{" "}
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={onOpenApiKeysDialog}
|
||||||
|
className="h-auto p-0 text-xs text-emerald-400"
|
||||||
|
>
|
||||||
|
register API keys
|
||||||
|
</Button>{" "}
|
||||||
|
before creating an agent.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="model" className="text-right text-neutral-300">
|
||||||
|
Model
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={values.model}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
model: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="col-span-3 bg-[#222] border-[#444] text-white">
|
||||||
|
<SelectValue placeholder="Select the model" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-[#222] border-[#444] text-white p-0">
|
||||||
|
<div className="sticky top-0 z-10 p-2 bg-[#222] border-b border-[#444]">
|
||||||
|
<Input
|
||||||
|
placeholder="Search models..."
|
||||||
|
className="bg-[#333] border-[#444] text-white h-8"
|
||||||
|
onChange={(e) => {
|
||||||
|
const searchQuery = e.target.value.toLowerCase();
|
||||||
|
const items = document.querySelectorAll('[data-model-item="true"]');
|
||||||
|
items.forEach((item) => {
|
||||||
|
const text = item.textContent?.toLowerCase() || '';
|
||||||
|
if (text.includes(searchQuery)) {
|
||||||
|
(item as HTMLElement).style.display = 'flex';
|
||||||
|
} else {
|
||||||
|
(item as HTMLElement).style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-[200px] overflow-y-auto py-1">
|
||||||
|
{availableModels
|
||||||
|
.filter((model) => {
|
||||||
|
if (!values.api_key_id) return true;
|
||||||
|
|
||||||
|
const selectedKey = apiKeys.find(
|
||||||
|
(key) => key.id === values.api_key_id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!selectedKey) return true;
|
||||||
|
|
||||||
|
return model.provider === selectedKey.provider;
|
||||||
|
})
|
||||||
|
.map((model) => (
|
||||||
|
<SelectItem
|
||||||
|
key={model.value}
|
||||||
|
value={model.value}
|
||||||
|
className="data-[selected]:bg-[#333] data-[highlighted]:bg-[#333] !text-white focus:!text-white hover:text-emerald-400 data-[selected]:!text-emerald-400"
|
||||||
|
data-model-item="true"
|
||||||
|
>
|
||||||
|
{model.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="instruction" className="text-right text-neutral-300">
|
||||||
|
Instructions
|
||||||
|
</Label>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<div className="relative">
|
||||||
|
<Textarea
|
||||||
|
id="instruction"
|
||||||
|
value={instructionText}
|
||||||
|
onChange={handleInstructionChange}
|
||||||
|
className="w-full bg-[#222] border-[#444] text-white pr-10"
|
||||||
|
rows={4}
|
||||||
|
onClick={handleExpandInstruction}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute top-3 right-5 text-neutral-400 hover:text-emerald-400 focus:outline-none"
|
||||||
|
onClick={handleExpandInstruction}
|
||||||
|
>
|
||||||
|
<Maximize2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-xs text-neutral-400">
|
||||||
|
<span className="inline-block h-3 w-3 mr-1">ℹ️</span>
|
||||||
|
<span>
|
||||||
|
Characters like {"{"} and {"}"} or {"{{"} and {"}}"} are automatically escaped to avoid errors in Python.
|
||||||
|
<span className="ml-2 text-emerald-400">Click to expand editor.</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Expanded Instruction Modal */}
|
||||||
|
<Dialog open={isInstructionModalOpen} onOpenChange={setIsInstructionModalOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[1200px] max-h-[90vh] bg-[#1a1a1a] border-[#333] overflow-hidden flex flex-col">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-white">Agent Instructions</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden flex flex-col min-h-[60vh]">
|
||||||
|
<Textarea
|
||||||
|
value={expandedInstructionText}
|
||||||
|
onChange={(e) => setExpandedInstructionText(e.target.value)}
|
||||||
|
className="flex-1 min-h-full bg-[#222] border-[#444] text-white p-4 focus:border-emerald-400 focus:ring-emerald-400 focus:ring-opacity-50 resize-none"
|
||||||
|
placeholder="Enter detailed instructions for the agent..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsInstructionModalOpen(false)}
|
||||||
|
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveExpandedInstruction}
|
||||||
|
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||||
|
>
|
||||||
|
<Save className="h-4 w-4 mr-2" />
|
||||||
|
Save Instructions
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
133
frontend/app/agents/config/LoopAgentConfig copy.tsx
Normal file
133
frontend/app/agents/config/LoopAgentConfig copy.tsx
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/config/LoopAgentConfig.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Agent } from "@/types/agent";
|
||||||
|
import { ArrowRight } from "lucide-react";
|
||||||
|
|
||||||
|
interface LoopAgentConfigProps {
|
||||||
|
values: {
|
||||||
|
config?: {
|
||||||
|
sub_agents?: string[];
|
||||||
|
max_iterations?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
onChange: (values: any) => void;
|
||||||
|
agents: Agent[];
|
||||||
|
getAgentNameById: (id: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoopAgentConfig({
|
||||||
|
values,
|
||||||
|
onChange,
|
||||||
|
agents,
|
||||||
|
getAgentNameById,
|
||||||
|
}: LoopAgentConfigProps) {
|
||||||
|
const handleMaxIterationsChange = (value: string) => {
|
||||||
|
const maxIterations = parseInt(value);
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
config: {
|
||||||
|
...values.config,
|
||||||
|
max_iterations: isNaN(maxIterations) ? undefined : maxIterations,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="max_iterations" className="text-right text-neutral-300">
|
||||||
|
Max. Iterations
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="max_iterations"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
value={values.config?.max_iterations || ""}
|
||||||
|
onChange={(e) => handleMaxIterationsChange(e.target.value)}
|
||||||
|
className="col-span-3 bg-[#222] border-[#444] text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||||
|
<h3 className="text-sm font-medium text-white mb-4">
|
||||||
|
Execution Order of Agents
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{values.config?.sub_agents && values.config.sub_agents.length > 0 ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{values.config.sub_agents.map((agentId, index) => (
|
||||||
|
<div
|
||||||
|
key={agentId}
|
||||||
|
className="flex items-center space-x-2 bg-[#2a2a2a] p-3 rounded-md"
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium text-white">
|
||||||
|
{getAgentNameById(agentId)}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-neutral-400">
|
||||||
|
Executed on{" "}
|
||||||
|
<Badge className="bg-[#333] text-emerald-400 border-none">
|
||||||
|
Position {index + 1}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-6 text-neutral-400">
|
||||||
|
Add agents in the "Sub-Agents" tab to define the execution order
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-3 text-sm text-neutral-400">
|
||||||
|
<p>
|
||||||
|
The agents will be executed sequentially in the order listed above.
|
||||||
|
The output of each agent will be provided as input to the next
|
||||||
|
agent in the sequence.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
125
frontend/app/agents/config/ParallelAgentConfig.tsx
Normal file
125
frontend/app/agents/config/ParallelAgentConfig.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/config/ParallelAgentConfig.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Agent } from "@/types/agent";
|
||||||
|
import { GitBranch } from "lucide-react";
|
||||||
|
|
||||||
|
interface ParallelAgentConfigProps {
|
||||||
|
values: {
|
||||||
|
config?: {
|
||||||
|
sub_agents?: string[];
|
||||||
|
aggregation_method?: string;
|
||||||
|
timeout_seconds?: number;
|
||||||
|
custom_mcp_servers?: any[];
|
||||||
|
wait_for_all?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
onChange: (values: any) => void;
|
||||||
|
agents: Agent[];
|
||||||
|
getAgentNameById: (id: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ParallelAgentConfig({
|
||||||
|
values,
|
||||||
|
onChange,
|
||||||
|
agents,
|
||||||
|
getAgentNameById,
|
||||||
|
}: ParallelAgentConfigProps) {
|
||||||
|
const aggregationMethods = [
|
||||||
|
{ value: "merge", label: "Merge all responses" },
|
||||||
|
{ value: "first", label: "Use only the first response" },
|
||||||
|
{ value: "last", label: "Use only the last response" },
|
||||||
|
{ value: "custom", label: "Custom aggregation" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTimeoutChange = (value: string) => {
|
||||||
|
const timeout = parseInt(value);
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
config: {
|
||||||
|
...values.config,
|
||||||
|
timeout_seconds: isNaN(timeout) ? undefined : timeout,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
|
||||||
|
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||||
|
<h3 className="text-sm font-medium text-white mb-4">
|
||||||
|
Agents in Parallel
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{values.config?.sub_agents && values.config.sub_agents.length > 0 ? (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
{values.config.sub_agents.map((agentId) => (
|
||||||
|
<div
|
||||||
|
key={agentId}
|
||||||
|
className="flex items-center space-x-2 bg-[#2a2a2a] p-3 rounded-md"
|
||||||
|
>
|
||||||
|
<GitBranch className="h-5 w-5 text-emerald-400" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium text-white truncate">
|
||||||
|
{getAgentNameById(agentId)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-6 text-neutral-400">
|
||||||
|
Add agents in the "Sub-Agents" tab to execute in parallel
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-3 text-sm text-neutral-400">
|
||||||
|
<p>
|
||||||
|
All listed agents will be executed simultaneously with the same
|
||||||
|
input. The responses will be aggregated according to the selected
|
||||||
|
method.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
120
frontend/app/agents/config/SequentialAgentConfig.tsx
Normal file
120
frontend/app/agents/config/SequentialAgentConfig.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/config/SequentialAgentConfig.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Agent } from "@/types/agent";
|
||||||
|
import { ArrowRight } from "lucide-react";
|
||||||
|
|
||||||
|
interface SequentialAgentConfigProps {
|
||||||
|
values: {
|
||||||
|
config?: {
|
||||||
|
sub_agents?: string[];
|
||||||
|
max_iterations?: number;
|
||||||
|
custom_mcp_servers?: any[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
onChange: (values: any) => void;
|
||||||
|
agents: Agent[];
|
||||||
|
getAgentNameById: (id: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SequentialAgentConfig({
|
||||||
|
values,
|
||||||
|
onChange,
|
||||||
|
agents,
|
||||||
|
getAgentNameById,
|
||||||
|
}: SequentialAgentConfigProps) {
|
||||||
|
const handleMaxIterationsChange = (value: string) => {
|
||||||
|
const maxIterations = parseInt(value);
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
config: {
|
||||||
|
...values.config,
|
||||||
|
max_iterations: isNaN(maxIterations) ? undefined : maxIterations,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
|
||||||
|
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||||
|
<h3 className="text-sm font-medium text-white mb-4">
|
||||||
|
Execution Order of Agents
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{values.config?.sub_agents && values.config.sub_agents.length > 0 ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{values.config.sub_agents.map((agentId, index) => (
|
||||||
|
<div
|
||||||
|
key={agentId}
|
||||||
|
className="flex items-center space-x-2 bg-[#2a2a2a] p-3 rounded-md"
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium text-white">
|
||||||
|
{getAgentNameById(agentId)}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-neutral-400">
|
||||||
|
Executed on{" "}
|
||||||
|
<Badge className="bg-[#333] text-emerald-400 border-none">
|
||||||
|
Position {index + 1}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-6 text-neutral-400">
|
||||||
|
Add agents in the "Sub-Agents" tab to define the execution order
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-3 text-sm text-neutral-400">
|
||||||
|
<p>
|
||||||
|
The agents will be executed sequentially in the order listed above.
|
||||||
|
The output of each agent will be provided as input to the next
|
||||||
|
agent in the sequence.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
801
frontend/app/agents/config/TaskAgentConfig.tsx
Normal file
801
frontend/app/agents/config/TaskAgentConfig.tsx
Normal file
@@ -0,0 +1,801 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/config/TaskAgentConfig.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Agent, TaskConfig } from "@/types/agent";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import {
|
||||||
|
Maximize2,
|
||||||
|
Save,
|
||||||
|
X,
|
||||||
|
ArrowDown,
|
||||||
|
List,
|
||||||
|
Search,
|
||||||
|
Edit,
|
||||||
|
PenTool,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
|
||||||
|
interface TaskAgentConfigProps {
|
||||||
|
values: Partial<Agent>;
|
||||||
|
onChange: (values: Partial<Agent>) => void;
|
||||||
|
agents: Agent[];
|
||||||
|
getAgentNameById: (id: string) => string;
|
||||||
|
singleTask?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAgentTypeLabel = (type: string): string => {
|
||||||
|
const typeMap: Record<string, string> = {
|
||||||
|
llm: "LLM",
|
||||||
|
a2a: "A2A",
|
||||||
|
sequential: "Sequential",
|
||||||
|
parallel: "Parallel",
|
||||||
|
loop: "Loop",
|
||||||
|
workflow: "Workflow",
|
||||||
|
task: "Task",
|
||||||
|
};
|
||||||
|
return typeMap[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAgentTypeColor = (type: string): string => {
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
llm: "bg-blue-800 text-white",
|
||||||
|
a2a: "bg-purple-800 text-white",
|
||||||
|
sequential: "bg-orange-800 text-white",
|
||||||
|
parallel: "bg-green-800 text-white",
|
||||||
|
loop: "bg-pink-800 text-white",
|
||||||
|
workflow: "bg-yellow-800 text-black",
|
||||||
|
task: "bg-green-800 text-white",
|
||||||
|
};
|
||||||
|
return colorMap[type] || "bg-neutral-800 text-white";
|
||||||
|
};
|
||||||
|
|
||||||
|
export function TaskAgentConfig({
|
||||||
|
values,
|
||||||
|
onChange,
|
||||||
|
agents,
|
||||||
|
getAgentNameById,
|
||||||
|
singleTask = false,
|
||||||
|
}: TaskAgentConfigProps) {
|
||||||
|
const [newTask, setNewTask] = useState<TaskConfig>({
|
||||||
|
agent_id: "",
|
||||||
|
description: "",
|
||||||
|
expected_output: "",
|
||||||
|
enabled_tools: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const [taskAgentSearchQuery, setTaskAgentSearchQuery] = useState<string>("");
|
||||||
|
const [filteredTaskAgents, setFilteredTaskAgents] = useState<Agent[]>([]);
|
||||||
|
const [isDescriptionModalOpen, setIsDescriptionModalOpen] = useState(false);
|
||||||
|
const [expandedDescription, setExpandedDescription] = useState("");
|
||||||
|
const [editingTaskIndex, setEditingTaskIndex] = useState<number | null>(null);
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
|
const [toolSearchQuery, setToolSearchQuery] = useState<string>("");
|
||||||
|
const [filteredTools, setFilteredTools] = useState<{id: string, name: string}[]>([]);
|
||||||
|
const [isToolsModalOpen, setIsToolsModalOpen] = useState(false);
|
||||||
|
const [tempSelectedTools, setTempSelectedTools] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isToolsModalOpen) {
|
||||||
|
if (isEditing && editingTaskIndex !== null && values.config?.tasks) {
|
||||||
|
setTempSelectedTools(
|
||||||
|
values.config.tasks[editingTaskIndex]?.enabled_tools || []
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setTempSelectedTools([...newTask.enabled_tools || []]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isToolsModalOpen]);
|
||||||
|
|
||||||
|
const getAvailableTaskAgents = (currentTaskAgentId?: string) =>
|
||||||
|
agents.filter(
|
||||||
|
(agent) =>
|
||||||
|
agent.id !== values.id &&
|
||||||
|
(!values.config?.tasks?.some((task) => task.agent_id === agent.id) ||
|
||||||
|
agent.id === currentTaskAgentId)
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentTaskAgentId =
|
||||||
|
isEditing && editingTaskIndex !== null && values.config?.tasks
|
||||||
|
? values.config.tasks[editingTaskIndex].agent_id
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const availableAgents = getAvailableTaskAgents(currentTaskAgentId);
|
||||||
|
|
||||||
|
if (taskAgentSearchQuery.trim() === "") {
|
||||||
|
setFilteredTaskAgents(availableAgents);
|
||||||
|
} else {
|
||||||
|
const query = taskAgentSearchQuery.toLowerCase();
|
||||||
|
setFilteredTaskAgents(
|
||||||
|
availableAgents.filter(
|
||||||
|
(agent) =>
|
||||||
|
agent.name.toLowerCase().includes(query) ||
|
||||||
|
(agent.description?.toLowerCase() || "").includes(query)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
taskAgentSearchQuery,
|
||||||
|
agents,
|
||||||
|
values.config?.tasks,
|
||||||
|
isEditing,
|
||||||
|
editingTaskIndex,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Reset editing state when values change externally
|
||||||
|
if (!isEditing) {
|
||||||
|
const currentTaskAgentId =
|
||||||
|
editingTaskIndex !== null && values.config?.tasks
|
||||||
|
? values.config.tasks[editingTaskIndex]?.agent_id
|
||||||
|
: undefined;
|
||||||
|
setFilteredTaskAgents(getAvailableTaskAgents(currentTaskAgentId));
|
||||||
|
}
|
||||||
|
}, [agents, values.config?.tasks]);
|
||||||
|
|
||||||
|
const getAvailableTools = () => {
|
||||||
|
if (!values.config?.tasks || values.config.tasks.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskAgentIds = values.config.tasks.map(task => task.agent_id);
|
||||||
|
|
||||||
|
const toolsList: {id: string, name: string}[] = [];
|
||||||
|
const toolsMap: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
taskAgentIds.forEach(agentId => {
|
||||||
|
const agent = agents.find(a => a.id === agentId);
|
||||||
|
|
||||||
|
if (agent?.type === "llm" && agent.config?.tools) {
|
||||||
|
agent.config.tools.forEach(tool => {
|
||||||
|
if (!toolsMap[tool.id]) {
|
||||||
|
toolsList.push({ id: tool.id, name: tool.id });
|
||||||
|
toolsMap[tool.id] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agent?.type === "llm" && agent.config?.mcp_servers) {
|
||||||
|
agent.config.mcp_servers.forEach(mcp => {
|
||||||
|
if (mcp.tools) {
|
||||||
|
mcp.tools.forEach(toolId => {
|
||||||
|
if (!toolsMap[toolId]) {
|
||||||
|
toolsList.push({ id: toolId, name: toolId });
|
||||||
|
toolsMap[toolId] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return toolsList;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const availableTools = getAvailableTools();
|
||||||
|
|
||||||
|
if (toolSearchQuery.trim() === "") {
|
||||||
|
setFilteredTools(availableTools);
|
||||||
|
} else {
|
||||||
|
const query = toolSearchQuery.toLowerCase();
|
||||||
|
setFilteredTools(
|
||||||
|
availableTools.filter(
|
||||||
|
(tool) =>
|
||||||
|
tool.name.toLowerCase().includes(query) ||
|
||||||
|
tool.id.toLowerCase().includes(query)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [toolSearchQuery, values.config?.tasks, agents]);
|
||||||
|
|
||||||
|
const handleAddTask = () => {
|
||||||
|
if (!newTask.agent_id || !newTask.description) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditing && editingTaskIndex !== null) {
|
||||||
|
const tasks = [...(values.config?.tasks || [])];
|
||||||
|
tasks[editingTaskIndex] = { ...newTask };
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
config: {
|
||||||
|
...(values.config || {}),
|
||||||
|
tasks,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsEditing(false);
|
||||||
|
setEditingTaskIndex(null);
|
||||||
|
} else {
|
||||||
|
const tasks = [...(values.config?.tasks || [])];
|
||||||
|
|
||||||
|
if (singleTask) {
|
||||||
|
tasks.splice(0, tasks.length, newTask);
|
||||||
|
} else {
|
||||||
|
tasks.push(newTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
config: {
|
||||||
|
...(values.config || {}),
|
||||||
|
tasks,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setNewTask({
|
||||||
|
agent_id: "",
|
||||||
|
description: "",
|
||||||
|
expected_output: "",
|
||||||
|
enabled_tools: [],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditTask = (index: number) => {
|
||||||
|
const task = values.config?.tasks?.[index];
|
||||||
|
if (task) {
|
||||||
|
setNewTask({ ...task });
|
||||||
|
setIsEditing(true);
|
||||||
|
setEditingTaskIndex(index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelEdit = () => {
|
||||||
|
setNewTask({
|
||||||
|
agent_id: "",
|
||||||
|
description: "",
|
||||||
|
expected_output: "",
|
||||||
|
enabled_tools: [],
|
||||||
|
});
|
||||||
|
setIsEditing(false);
|
||||||
|
setEditingTaskIndex(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveTask = (index: number) => {
|
||||||
|
if (editingTaskIndex === index) {
|
||||||
|
handleCancelEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasks = [...(values.config?.tasks || [])];
|
||||||
|
tasks.splice(index, 1);
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
config: {
|
||||||
|
...(values.config || {}),
|
||||||
|
tasks,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDescriptionChange = (
|
||||||
|
e: React.ChangeEvent<HTMLTextAreaElement>
|
||||||
|
) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
setNewTask({
|
||||||
|
...newTask,
|
||||||
|
description: newValue,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExpandDescription = () => {
|
||||||
|
setExpandedDescription(newTask.description);
|
||||||
|
setIsDescriptionModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveExpandedDescription = () => {
|
||||||
|
setNewTask({
|
||||||
|
...newTask,
|
||||||
|
description: expandedDescription,
|
||||||
|
});
|
||||||
|
setIsDescriptionModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleTool = (toolId: string) => {
|
||||||
|
const index = tempSelectedTools.indexOf(toolId);
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
|
setTempSelectedTools(tempSelectedTools.filter(id => id !== toolId));
|
||||||
|
} else {
|
||||||
|
setTempSelectedTools([...tempSelectedTools, toolId]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isToolEnabled = (toolId: string) => {
|
||||||
|
return tempSelectedTools.includes(toolId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveTools = () => {
|
||||||
|
if (isEditing && editingTaskIndex !== null && values.config?.tasks) {
|
||||||
|
const tasks = [...(values.config?.tasks || [])];
|
||||||
|
|
||||||
|
const updatedTask = {
|
||||||
|
...tasks[editingTaskIndex],
|
||||||
|
enabled_tools: [...tempSelectedTools]
|
||||||
|
};
|
||||||
|
|
||||||
|
tasks[editingTaskIndex] = updatedTask;
|
||||||
|
|
||||||
|
const newConfig = {
|
||||||
|
...(values.config || {}),
|
||||||
|
tasks: tasks
|
||||||
|
};
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
config: newConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
} else if (newTask.agent_id) {
|
||||||
|
const updatedNewTask = {
|
||||||
|
...newTask,
|
||||||
|
enabled_tools: [...tempSelectedTools]
|
||||||
|
};
|
||||||
|
|
||||||
|
setNewTask(updatedNewTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsToolsModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderAgentTypeBadge = (agentId: string) => {
|
||||||
|
const agent = agents.find((a) => a.id === agentId);
|
||||||
|
if (!agent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge className={`ml-2 ${getAgentTypeColor(agent.type)} text-xs`}>
|
||||||
|
{getAgentTypeLabel(agent.type)}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-medium text-white flex items-center">
|
||||||
|
<List className="mr-2 h-5 w-5 text-emerald-400" />
|
||||||
|
{singleTask ? "Task" : "Tasks"}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||||
|
<p className="text-sm text-neutral-400 mb-4">
|
||||||
|
{singleTask
|
||||||
|
? "Configure the task that will be executed by the agent."
|
||||||
|
: "Configure the sequential tasks that will be executed by the team of agents."}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{values.config?.tasks && values.config.tasks.length > 0 ? (
|
||||||
|
<div className="space-y-4 mb-4">
|
||||||
|
{values.config.tasks.map((task, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`border border-[#333] rounded-md p-3 ${
|
||||||
|
editingTaskIndex === index ? "bg-[#1e3a3a]" : "bg-[#2a2a2a]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="inline-flex items-center justify-center rounded-full bg-[#333] px-2 py-1 text-xs text-white mr-2">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
<h4 className="font-medium text-white flex items-center">
|
||||||
|
{getAgentNameById(task.agent_id)}
|
||||||
|
{renderAgentTypeBadge(task.agent_id)}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-neutral-300 mt-1">
|
||||||
|
{task.description}
|
||||||
|
</p>
|
||||||
|
{task.expected_output && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<span className="text-xs text-neutral-400">
|
||||||
|
Expected output:
|
||||||
|
</span>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="ml-2 bg-[#333] text-emerald-400 border-emerald-400/30"
|
||||||
|
>
|
||||||
|
{task.expected_output}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{task.enabled_tools && task.enabled_tools.length > 0 && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<span className="text-xs text-neutral-400">
|
||||||
|
Enabled tools:
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
|
{task.enabled_tools.map((toolId) => (
|
||||||
|
<Badge
|
||||||
|
key={toolId}
|
||||||
|
className="bg-[#333] text-emerald-400 border border-emerald-400/30 text-xs"
|
||||||
|
>
|
||||||
|
{toolId}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleEditTask(index)}
|
||||||
|
className="text-neutral-400 hover:text-emerald-400 hover:bg-[#333] mr-1"
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleRemoveTask(index)}
|
||||||
|
className="text-red-500 hover:text-red-400 hover:bg-[#333]"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!singleTask &&
|
||||||
|
index < (values.config?.tasks?.length || 0) - 1 && (
|
||||||
|
<div className="flex justify-center my-2">
|
||||||
|
<ArrowDown className="h-4 w-4 text-neutral-400" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-4 mb-4 bg-[#2a2a2a] rounded-md">
|
||||||
|
<p className="text-neutral-400">No tasks configured</p>
|
||||||
|
<p className="text-xs text-neutral-500">
|
||||||
|
{singleTask
|
||||||
|
? "Add a task to define the agent's behavior"
|
||||||
|
: "Add tasks to define the workflow of the team"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(!singleTask ||
|
||||||
|
!values.config?.tasks ||
|
||||||
|
values.config.tasks.length === 0 ||
|
||||||
|
isEditing) && (
|
||||||
|
<div className="space-y-3 border-t border-[#333] pt-4">
|
||||||
|
<h4 className="text-sm font-medium text-white flex items-center justify-between">
|
||||||
|
<span>
|
||||||
|
{isEditing
|
||||||
|
? "Edit task"
|
||||||
|
: `Add ${singleTask ? "one" : "new"} task`}
|
||||||
|
</span>
|
||||||
|
{isEditing && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleCancelEdit}
|
||||||
|
className="text-neutral-400 hover:text-emerald-400 hover:bg-[#333]"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4 mr-1" /> Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
htmlFor="agent_id"
|
||||||
|
className="text-xs text-neutral-400 mb-1 block"
|
||||||
|
>
|
||||||
|
Agent
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={newTask.agent_id}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setNewTask({ ...newTask, agent_id: value })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="bg-[#2a2a2a] border-[#444] text-white">
|
||||||
|
<SelectValue placeholder="Select agent" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-[#2a2a2a] border-[#444] text-white p-0">
|
||||||
|
<div className="sticky top-0 z-10 p-2 bg-[#2a2a2a] border-b border-[#444]">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-neutral-400" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search agents..."
|
||||||
|
className="bg-[#333] border-[#444] text-white h-8 pl-8"
|
||||||
|
value={taskAgentSearchQuery}
|
||||||
|
onChange={(e) =>
|
||||||
|
setTaskAgentSearchQuery(e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-h-[200px] overflow-y-auto py-1">
|
||||||
|
{filteredTaskAgents.length > 0 ? (
|
||||||
|
filteredTaskAgents.map((agent) => (
|
||||||
|
<SelectItem
|
||||||
|
key={agent.id}
|
||||||
|
value={agent.id}
|
||||||
|
className="hover:bg-[#333] focus:bg-[#333] flex items-center justify-between px-2"
|
||||||
|
data-agent-item="true"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="mr-2">{agent.name}</span>
|
||||||
|
<Badge
|
||||||
|
className={`${getAgentTypeColor(
|
||||||
|
agent.type
|
||||||
|
)} text-xs`}
|
||||||
|
>
|
||||||
|
{getAgentTypeLabel(agent.type)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-neutral-500 px-4 py-2 text-center">
|
||||||
|
No agents found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-2">
|
||||||
|
<Label
|
||||||
|
htmlFor="description"
|
||||||
|
className="text-xs text-neutral-400 mb-1 block"
|
||||||
|
>
|
||||||
|
Task description
|
||||||
|
</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
value={newTask.description}
|
||||||
|
onChange={handleDescriptionChange}
|
||||||
|
className="w-full bg-[#2a2a2a] border-[#444] text-white pr-10"
|
||||||
|
rows={3}
|
||||||
|
onClick={handleExpandDescription}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute top-3 right-5 text-neutral-400 hover:text-emerald-400 focus:outline-none"
|
||||||
|
onClick={handleExpandDescription}
|
||||||
|
>
|
||||||
|
<Maximize2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-xs text-neutral-400">
|
||||||
|
<span className="inline-block h-3 w-3 mr-1">ℹ️</span>
|
||||||
|
<span>
|
||||||
|
Use {"{"}content{"}"} to insert the user's input.
|
||||||
|
<span className="ml-2 text-emerald-400">
|
||||||
|
Click to expand editor.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
htmlFor="expected_output"
|
||||||
|
className="text-xs text-neutral-400 mb-1 block"
|
||||||
|
>
|
||||||
|
Expected output (optional)
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="expected_output"
|
||||||
|
placeholder="Ex: JSON report, List of recommendations, etc."
|
||||||
|
value={newTask.expected_output}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewTask({ ...newTask, expected_output: e.target.value })
|
||||||
|
}
|
||||||
|
className="bg-[#2a2a2a] border-[#444] text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{newTask.enabled_tools && newTask.enabled_tools.length > 0 && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<Label className="text-xs text-neutral-400 mb-1 block">
|
||||||
|
Selected tools:
|
||||||
|
</Label>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{newTask.enabled_tools.map((toolId) => (
|
||||||
|
<Badge
|
||||||
|
key={toolId}
|
||||||
|
className="bg-[#333] text-emerald-400 border border-emerald-400/30"
|
||||||
|
>
|
||||||
|
{toolId}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between mt-3">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (newTask.agent_id) setIsToolsModalOpen(true);
|
||||||
|
}}
|
||||||
|
disabled={!newTask.agent_id}
|
||||||
|
className="border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 px-3"
|
||||||
|
>
|
||||||
|
<PenTool className="h-4 w-4 mr-2" />
|
||||||
|
Configure tools
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleAddTask}
|
||||||
|
disabled={!newTask.agent_id || !newTask.description}
|
||||||
|
className="bg-[#222] text-emerald-400 border border-emerald-400 hover:bg-emerald-400/10"
|
||||||
|
>
|
||||||
|
<Save className="h-4 w-4 mr-1" />{" "}
|
||||||
|
{isEditing ? "Update task" : "Add task"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={isDescriptionModalOpen}
|
||||||
|
onOpenChange={setIsDescriptionModalOpen}
|
||||||
|
>
|
||||||
|
<DialogContent className="sm:max-w-[1200px] max-h-[90vh] bg-[#1a1a1a] border-[#333] overflow-hidden flex flex-col">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-white">Task Description</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden flex flex-col min-h-[60vh]">
|
||||||
|
<Textarea
|
||||||
|
value={expandedDescription}
|
||||||
|
onChange={(e) => setExpandedDescription(e.target.value)}
|
||||||
|
className="flex-1 min-h-full bg-[#222] border-[#444] text-white p-4 focus:border-emerald-400 focus:ring-emerald-400 focus:ring-opacity-50 resize-none"
|
||||||
|
placeholder="Enter detailed description for the task..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsDescriptionModalOpen(false)}
|
||||||
|
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveExpandedDescription}
|
||||||
|
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||||
|
>
|
||||||
|
<Save className="h-4 w-4 mr-2" />
|
||||||
|
Save description
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={isToolsModalOpen} onOpenChange={setIsToolsModalOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[600px] max-h-[90vh] bg-[#1a1a1a] border-[#333] overflow-hidden flex flex-col">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-white">
|
||||||
|
Available tools
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="relative mb-4">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-neutral-400" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search tools..."
|
||||||
|
className="bg-[#222] border-[#444] text-white pl-9"
|
||||||
|
value={toolSearchQuery}
|
||||||
|
onChange={(e) => setToolSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto space-y-2 pr-1">
|
||||||
|
{filteredTools.length > 0 ? (
|
||||||
|
filteredTools.map((tool) => (
|
||||||
|
<div
|
||||||
|
key={tool.id}
|
||||||
|
className="flex items-center space-x-2 p-2 rounded-md hover:bg-[#333] transition duration-150"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={tool.id}
|
||||||
|
checked={isToolEnabled(tool.id)}
|
||||||
|
onCheckedChange={() => handleToggleTool(tool.id)}
|
||||||
|
className="border-[#444] data-[state=checked]:bg-emerald-400 data-[state=checked]:text-black"
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor={tool.id}
|
||||||
|
className="cursor-pointer text-white flex-1"
|
||||||
|
>
|
||||||
|
{tool.name}
|
||||||
|
</Label>
|
||||||
|
<Badge className="bg-[#333] text-emerald-400">{tool.id}</Badge>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<p className="text-neutral-400">No tools available</p>
|
||||||
|
<p className="text-xs text-neutral-500">
|
||||||
|
The tools are obtained from the selected agents in the tasks.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveTools}
|
||||||
|
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||||
|
>
|
||||||
|
<Save className="h-4 w-4 mr-2" />
|
||||||
|
Save settings
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
184
frontend/app/agents/dialogs/AgentToolDialog.tsx
Normal file
184
frontend/app/agents/dialogs/AgentToolDialog.tsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/dialogs/AgentToolDialog.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Agent } from "@/types/agent";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { listAgents } from "@/services/agentService";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
|
interface AgentToolDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSave: (tool: { id: string; envs: Record<string, string> }) => void;
|
||||||
|
currentAgentId?: string;
|
||||||
|
folderId?: string | null;
|
||||||
|
clientId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgentToolDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onSave,
|
||||||
|
currentAgentId,
|
||||||
|
folderId,
|
||||||
|
clientId,
|
||||||
|
}: AgentToolDialogProps) {
|
||||||
|
const [selectedAgentId, setSelectedAgentId] = useState<string>("");
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [agents, setAgents] = useState<Agent[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setSelectedAgentId("");
|
||||||
|
setSearch("");
|
||||||
|
loadAgents();
|
||||||
|
}
|
||||||
|
}, [open, folderId, clientId]);
|
||||||
|
|
||||||
|
const loadAgents = async () => {
|
||||||
|
if (!clientId) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await listAgents(
|
||||||
|
clientId,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
folderId || undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter out the current agent to avoid self-reference
|
||||||
|
const filteredAgents = res.data.filter(agent =>
|
||||||
|
agent.id !== currentAgentId
|
||||||
|
);
|
||||||
|
|
||||||
|
setAgents(filteredAgents);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading agents:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!selectedAgentId) return;
|
||||||
|
onSave({ id: selectedAgentId, envs: {} });
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredAgents = agents.filter((agent) =>
|
||||||
|
agent.name.toLowerCase().includes(search.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-[420px] max-h-[90vh] overflow-hidden flex flex-col bg-[#1a1a1a] border-[#333]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-white">Add Agent Tool</DialogTitle>
|
||||||
|
<DialogDescription className="text-neutral-400">
|
||||||
|
Select an agent to add as a tool.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex-1 overflow-auto px-2 pb-2 space-y-4">
|
||||||
|
<Input
|
||||||
|
placeholder="Search agent by name..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
className="mb-2 bg-[#222] border-[#444] text-white placeholder:text-neutral-400"
|
||||||
|
/>
|
||||||
|
<div className="space-y-2 max-h-60 overflow-y-auto pr-1">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-6">
|
||||||
|
<Loader2 className="h-6 w-6 text-emerald-400 animate-spin" />
|
||||||
|
<div className="mt-2 text-sm text-neutral-400">Loading agents...</div>
|
||||||
|
</div>
|
||||||
|
) : filteredAgents.length === 0 ? (
|
||||||
|
<div className="text-neutral-400 text-sm text-center py-6">
|
||||||
|
{search ? `No agents found matching "${search}"` : "No agents found in this folder."}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredAgents.map((agent) => (
|
||||||
|
<button
|
||||||
|
key={agent.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedAgentId(agent.id)}
|
||||||
|
className={cn(
|
||||||
|
"w-full flex items-start gap-3 p-3 rounded-md border border-[#333] bg-[#232323] hover:bg-[#222] transition text-left cursor-pointer",
|
||||||
|
selectedAgentId === agent.id && "border-emerald-400 bg-[#1a1a1a] shadow-md"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium text-white text-base">{agent.name}</div>
|
||||||
|
<div className="text-xs text-neutral-400 mt-1">
|
||||||
|
{agent.description || "No description"}
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-neutral-500 mt-1">ID: {agent.id}</div>
|
||||||
|
</div>
|
||||||
|
{selectedAgentId === agent.id && (
|
||||||
|
<span className="ml-2 text-emerald-400 font-bold">Selected</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="p-4 pt-2 border-t border-[#333]">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||||
|
disabled={!selectedAgentId || isLoading}
|
||||||
|
>
|
||||||
|
Add Tool
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
445
frontend/app/agents/dialogs/ApiKeysDialog.tsx
Normal file
445
frontend/app/agents/dialogs/ApiKeysDialog.tsx
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/dialogs/ApiKeysDialog.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { ConfirmationDialog } from "./ConfirmationDialog";
|
||||||
|
import { ApiKey } from "@/services/agentService";
|
||||||
|
import { Edit, Eye, Key, Plus, Trash2, X } from "lucide-react";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { availableModelProviders } from "@/types/aiModels";
|
||||||
|
|
||||||
|
interface ApiKeysDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
apiKeys: ApiKey[];
|
||||||
|
isLoading: boolean;
|
||||||
|
onAddApiKey: (apiKey: {
|
||||||
|
name: string;
|
||||||
|
provider: string;
|
||||||
|
key_value: string;
|
||||||
|
}) => Promise<void>;
|
||||||
|
onUpdateApiKey: (
|
||||||
|
id: string,
|
||||||
|
apiKey: {
|
||||||
|
name: string;
|
||||||
|
provider: string;
|
||||||
|
key_value?: string;
|
||||||
|
is_active: boolean;
|
||||||
|
}
|
||||||
|
) => Promise<void>;
|
||||||
|
onDeleteApiKey: (id: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApiKeysDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
apiKeys,
|
||||||
|
isLoading,
|
||||||
|
onAddApiKey,
|
||||||
|
onUpdateApiKey,
|
||||||
|
onDeleteApiKey,
|
||||||
|
}: ApiKeysDialogProps) {
|
||||||
|
const [isAddingApiKey, setIsAddingApiKey] = useState(false);
|
||||||
|
const [isEditingApiKey, setIsEditingApiKey] = useState(false);
|
||||||
|
const [currentApiKey, setCurrentApiKey] = useState<
|
||||||
|
Partial<ApiKey & { key_value?: string }>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
|
const [apiKeyToDelete, setApiKeyToDelete] = useState<ApiKey | null>(null);
|
||||||
|
const [isApiKeyVisible, setIsApiKeyVisible] = useState(false);
|
||||||
|
|
||||||
|
const handleAddClick = () => {
|
||||||
|
setCurrentApiKey({});
|
||||||
|
setIsAddingApiKey(true);
|
||||||
|
setIsEditingApiKey(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditClick = (apiKey: ApiKey) => {
|
||||||
|
setCurrentApiKey({ ...apiKey, key_value: "" });
|
||||||
|
setIsAddingApiKey(true);
|
||||||
|
setIsEditingApiKey(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteClick = (apiKey: ApiKey) => {
|
||||||
|
setApiKeyToDelete(apiKey);
|
||||||
|
setIsDeleteDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveApiKey = async () => {
|
||||||
|
if (
|
||||||
|
!currentApiKey.name ||
|
||||||
|
!currentApiKey.provider ||
|
||||||
|
(!isEditingApiKey && !currentApiKey.key_value)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (currentApiKey.id) {
|
||||||
|
await onUpdateApiKey(currentApiKey.id, {
|
||||||
|
name: currentApiKey.name,
|
||||||
|
provider: currentApiKey.provider,
|
||||||
|
key_value: currentApiKey.key_value,
|
||||||
|
is_active: currentApiKey.is_active !== false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await onAddApiKey({
|
||||||
|
name: currentApiKey.name,
|
||||||
|
provider: currentApiKey.provider,
|
||||||
|
key_value: currentApiKey.key_value!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentApiKey({});
|
||||||
|
setIsAddingApiKey(false);
|
||||||
|
setIsEditingApiKey(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error saving API key:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteConfirm = async () => {
|
||||||
|
if (!apiKeyToDelete) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onDeleteApiKey(apiKeyToDelete.id);
|
||||||
|
setApiKeyToDelete(null);
|
||||||
|
setIsDeleteDialogOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting API key:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-hidden flex flex-col bg-[#1a1a1a] border-[#333]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-white">Manage API Keys</DialogTitle>
|
||||||
|
<DialogDescription className="text-neutral-400">
|
||||||
|
Add and manage API keys for use in your agents
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-auto p-1">
|
||||||
|
{isAddingApiKey ? (
|
||||||
|
<div className="space-y-4 p-4 bg-[#222] rounded-md">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h3 className="text-lg font-medium text-white">
|
||||||
|
{isEditingApiKey ? "Edit Key" : "New Key"}
|
||||||
|
</h3>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setIsAddingApiKey(false);
|
||||||
|
setIsEditingApiKey(false);
|
||||||
|
setCurrentApiKey({});
|
||||||
|
}}
|
||||||
|
className="text-neutral-400 hover:text-white"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="name" className="text-right text-neutral-300">
|
||||||
|
Name
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={currentApiKey.name || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCurrentApiKey({
|
||||||
|
...currentApiKey,
|
||||||
|
name: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="col-span-3 bg-[#333] border-[#444] text-white"
|
||||||
|
placeholder="OpenAI GPT-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label
|
||||||
|
htmlFor="provider"
|
||||||
|
className="text-right text-neutral-300"
|
||||||
|
>
|
||||||
|
Provider
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={currentApiKey.provider}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setCurrentApiKey({
|
||||||
|
...currentApiKey,
|
||||||
|
provider: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="col-span-3 bg-[#333] border-[#444] text-white">
|
||||||
|
<SelectValue placeholder="Select Provider" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-[#222] border-[#444] text-white">
|
||||||
|
{availableModelProviders.map((provider) => (
|
||||||
|
<SelectItem
|
||||||
|
key={provider.value}
|
||||||
|
value={provider.value}
|
||||||
|
className="data-[selected]:bg-[#333] data-[highlighted]:bg-[#333] !text-white focus:!text-white hover:text-emerald-400 data-[selected]:!text-emerald-400"
|
||||||
|
>
|
||||||
|
{provider.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label
|
||||||
|
htmlFor="key_value"
|
||||||
|
className="text-right text-neutral-300"
|
||||||
|
>
|
||||||
|
Key Value
|
||||||
|
</Label>
|
||||||
|
<div className="col-span-3 relative">
|
||||||
|
<Input
|
||||||
|
id="key_value"
|
||||||
|
value={currentApiKey.key_value || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCurrentApiKey({
|
||||||
|
...currentApiKey,
|
||||||
|
key_value: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="bg-[#333] border-[#444] text-white pr-10"
|
||||||
|
type={isApiKeyVisible ? "text" : "password"}
|
||||||
|
placeholder={
|
||||||
|
isEditingApiKey
|
||||||
|
? "Leave blank to keep the current value"
|
||||||
|
: "sk-..."
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
className="absolute right-2 top-1/2 transform -translate-y-1/2 h-7 w-7 p-0 text-neutral-400 hover:text-white"
|
||||||
|
onClick={() => setIsApiKeyVisible(!isApiKeyVisible)}
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isEditingApiKey && (
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label
|
||||||
|
htmlFor="is_active"
|
||||||
|
className="text-right text-neutral-300"
|
||||||
|
>
|
||||||
|
Status
|
||||||
|
</Label>
|
||||||
|
<div className="col-span-3 flex items-center">
|
||||||
|
<Checkbox
|
||||||
|
id="is_active"
|
||||||
|
checked={currentApiKey.is_active !== false}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setCurrentApiKey({
|
||||||
|
...currentApiKey,
|
||||||
|
is_active: !!checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="mr-2 data-[state=checked]:bg-emerald-400 data-[state=checked]:border-emerald-400"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="is_active" className="text-neutral-300">
|
||||||
|
Active
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 mt-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setIsAddingApiKey(false);
|
||||||
|
setIsEditingApiKey(false);
|
||||||
|
setCurrentApiKey({});
|
||||||
|
}}
|
||||||
|
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveApiKey}
|
||||||
|
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading && (
|
||||||
|
<div className="animate-spin h-4 w-4 border-2 border-black border-t-transparent rounded-full mr-1"></div>
|
||||||
|
)}
|
||||||
|
{isEditingApiKey ? "Update" : "Add"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-lg font-medium text-white">
|
||||||
|
Available Keys
|
||||||
|
</h3>
|
||||||
|
<Button
|
||||||
|
onClick={handleAddClick}
|
||||||
|
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||||
|
>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
New Key
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center h-40">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-emerald-400"></div>
|
||||||
|
</div>
|
||||||
|
) : apiKeys.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{apiKeys.map((apiKey) => (
|
||||||
|
<div
|
||||||
|
key={apiKey.id}
|
||||||
|
className="flex items-center justify-between p-3 bg-[#222] rounded-md border border-[#333] hover:border-emerald-400/30"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-white">{apiKey.name}</p>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="bg-[#333] text-emerald-400 border-emerald-400/30"
|
||||||
|
>
|
||||||
|
{apiKey.provider.toUpperCase()}
|
||||||
|
</Badge>
|
||||||
|
<p className="text-xs text-neutral-400">
|
||||||
|
Created on{" "}
|
||||||
|
{new Date(apiKey.created_at).toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
{!apiKey.is_active && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="bg-[#333] text-red-400 border-red-400/30"
|
||||||
|
>
|
||||||
|
Inactive
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleEditClick(apiKey)}
|
||||||
|
className="text-neutral-300 hover:text-emerald-400 hover:bg-[#333]"
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDeleteClick(apiKey)}
|
||||||
|
className="text-red-500 hover:text-red-400 hover:bg-[#333]"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-10 border border-dashed border-[#333] rounded-md bg-[#222] text-neutral-400">
|
||||||
|
<Key className="mx-auto h-10 w-10 text-neutral-500 mb-3" />
|
||||||
|
<p>You don't have any API keys registered</p>
|
||||||
|
<p className="text-sm mt-1">
|
||||||
|
Add your API keys to use them in your agents
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={handleAddClick}
|
||||||
|
className="mt-4 bg-[#333] text-emerald-400 hover:bg-[#444]"
|
||||||
|
>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Add Key
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="border-t border-[#333] pt-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<ConfirmationDialog
|
||||||
|
open={isDeleteDialogOpen}
|
||||||
|
onOpenChange={setIsDeleteDialogOpen}
|
||||||
|
title="Confirm Delete"
|
||||||
|
description={`Are you sure you want to delete the key "${apiKeyToDelete?.name}"? This action cannot be undone.`}
|
||||||
|
confirmText="Delete"
|
||||||
|
confirmVariant="destructive"
|
||||||
|
onConfirm={handleDeleteConfirm}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
93
frontend/app/agents/dialogs/ConfirmationDialog.tsx
Normal file
93
frontend/app/agents/dialogs/ConfirmationDialog.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/dialogs/ConfirmationDialog.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
|
||||||
|
interface ConfirmationDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
cancelText?: string;
|
||||||
|
confirmText?: string;
|
||||||
|
confirmVariant?: "default" | "destructive";
|
||||||
|
onConfirm: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfirmationDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
cancelText = "Cancel",
|
||||||
|
confirmText = "Confirm",
|
||||||
|
confirmVariant = "default",
|
||||||
|
onConfirm,
|
||||||
|
}: ConfirmationDialogProps) {
|
||||||
|
return (
|
||||||
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<AlertDialogContent className="bg-[#1a1a1a] border-[#333] text-white">
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription className="text-neutral-400">
|
||||||
|
{description}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white">
|
||||||
|
{cancelText}
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onConfirm();
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
confirmVariant === "destructive"
|
||||||
|
? "bg-red-600 text-white hover:bg-red-700"
|
||||||
|
: "bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{confirmText}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
237
frontend/app/agents/dialogs/CustomMCPDialog.tsx
Normal file
237
frontend/app/agents/dialogs/CustomMCPDialog.tsx
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/dialogs/CustomMCPDialog.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { X, Plus } from "lucide-react";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
interface CustomMCPHeader {
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomMCPServer {
|
||||||
|
url: string;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomMCPDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSave: (customMCP: CustomMCPServer) => void;
|
||||||
|
initialCustomMCP?: CustomMCPServer | null;
|
||||||
|
clientId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CustomMCPDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onSave,
|
||||||
|
initialCustomMCP = null,
|
||||||
|
clientId,
|
||||||
|
}: CustomMCPDialogProps) {
|
||||||
|
const [customMCP, setCustomMCP] = useState<Partial<CustomMCPServer>>({
|
||||||
|
url: "",
|
||||||
|
headers: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [headersList, setHeadersList] = useState<CustomMCPHeader[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
if (initialCustomMCP) {
|
||||||
|
setCustomMCP(initialCustomMCP);
|
||||||
|
const headersList = Object.entries(initialCustomMCP.headers || {}).map(
|
||||||
|
([key, value], index) => ({
|
||||||
|
id: `header-${index}`,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setHeadersList(headersList);
|
||||||
|
} else {
|
||||||
|
setCustomMCP({ url: "", headers: {} });
|
||||||
|
setHeadersList([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [open, initialCustomMCP]);
|
||||||
|
|
||||||
|
const handleAddHeader = () => {
|
||||||
|
setHeadersList([
|
||||||
|
...headersList,
|
||||||
|
{ id: `header-${Date.now()}`, key: "", value: "" },
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveHeader = (id: string) => {
|
||||||
|
setHeadersList(headersList.filter((header) => header.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHeaderChange = (
|
||||||
|
id: string,
|
||||||
|
field: "key" | "value",
|
||||||
|
value: string
|
||||||
|
) => {
|
||||||
|
setHeadersList(
|
||||||
|
headersList.map((header) =>
|
||||||
|
header.id === id ? { ...header, [field]: value } : header
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!customMCP.url) return;
|
||||||
|
|
||||||
|
const headersObject: Record<string, string> = {};
|
||||||
|
headersList.forEach((header) => {
|
||||||
|
if (header.key.trim()) {
|
||||||
|
headersObject[header.key] = header.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onSave({
|
||||||
|
url: customMCP.url,
|
||||||
|
headers: headersObject,
|
||||||
|
});
|
||||||
|
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-hidden flex flex-col bg-[#1a1a1a] border-[#333]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-white">
|
||||||
|
Configure Custom MCP
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-neutral-400">
|
||||||
|
Configure the URL and HTTP headers for the custom MCP.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-auto p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="custom-mcp-url" className="text-neutral-300">
|
||||||
|
MCP URL
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="custom-mcp-url"
|
||||||
|
value={customMCP.url || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCustomMCP({
|
||||||
|
...customMCP,
|
||||||
|
url: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="bg-[#222] border-[#444] text-white"
|
||||||
|
placeholder="https://meu-servidor-mcp.com/api"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-sm font-medium text-white">HTTP Headers</h3>
|
||||||
|
<div className="border border-[#444] rounded-md p-3 bg-[#222]">
|
||||||
|
{headersList.map((header) => (
|
||||||
|
<div
|
||||||
|
key={header.id}
|
||||||
|
className="grid grid-cols-5 items-center gap-2 mb-2"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={header.key}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleHeaderChange(header.id, "key", e.target.value)
|
||||||
|
}
|
||||||
|
className="col-span-2 bg-[#333] border-[#444] text-white"
|
||||||
|
placeholder="Header Name"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={header.value}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleHeaderChange(header.id, "value", e.target.value)
|
||||||
|
}
|
||||||
|
className="col-span-2 bg-[#333] border-[#444] text-white"
|
||||||
|
placeholder="Header Value"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleRemoveHeader(header.id)}
|
||||||
|
className="col-span-1 h-8 text-red-500 hover:text-red-400 hover:bg-[#444]"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleAddHeader}
|
||||||
|
className="w-full mt-2 border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-1" /> Add Header
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="p-4 pt-2 border-t border-[#333]">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||||
|
disabled={!customMCP.url}
|
||||||
|
>
|
||||||
|
{initialCustomMCP?.url ? "Save Custom MCP" : "Add Custom MCP"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
864
frontend/app/agents/dialogs/CustomToolDialog.tsx
Normal file
864
frontend/app/agents/dialogs/CustomToolDialog.tsx
Normal file
@@ -0,0 +1,864 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/dialogs/CustomToolDialog.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import {
|
||||||
|
X,
|
||||||
|
Plus,
|
||||||
|
Info,
|
||||||
|
Trash,
|
||||||
|
Globe,
|
||||||
|
FileJson,
|
||||||
|
LayoutList,
|
||||||
|
Settings,
|
||||||
|
Database,
|
||||||
|
Code,
|
||||||
|
Server,
|
||||||
|
Wand
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { HTTPTool, HTTPToolParameter } from "@/types/agent";
|
||||||
|
import { sanitizeAgentName } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
|
interface CustomToolDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSave: (tool: HTTPTool) => void;
|
||||||
|
initialTool?: HTTPTool | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CustomToolDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onSave,
|
||||||
|
initialTool = null,
|
||||||
|
}: CustomToolDialogProps) {
|
||||||
|
const [tool, setTool] = useState<Partial<HTTPTool>>({
|
||||||
|
name: "",
|
||||||
|
method: "GET",
|
||||||
|
endpoint: "",
|
||||||
|
description: "",
|
||||||
|
headers: {},
|
||||||
|
values: {},
|
||||||
|
parameters: {},
|
||||||
|
error_handling: {
|
||||||
|
timeout: 30,
|
||||||
|
retry_count: 0,
|
||||||
|
fallback_response: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [headersList, setHeadersList] = useState<{ id: string; key: string; value: string }[]>([]);
|
||||||
|
const [bodyParams, setBodyParams] = useState<{ id: string; key: string; param: HTTPToolParameter }[]>([]);
|
||||||
|
const [pathParams, setPathParams] = useState<{ id: string; key: string; desc: string }[]>([]);
|
||||||
|
const [queryParams, setQueryParams] = useState<{ id: string; key: string; value: string }[]>([]);
|
||||||
|
const [valuesList, setValuesList] = useState<{ id: string; key: string; value: string }[]>([]);
|
||||||
|
const [timeout, setTimeout] = useState<number>(30);
|
||||||
|
const [fallbackError, setFallbackError] = useState<string>("");
|
||||||
|
const [fallbackMessage, setFallbackMessage] = useState<string>("");
|
||||||
|
const [activeTab, setActiveTab] = useState("basics");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
if (initialTool) {
|
||||||
|
setTool(initialTool);
|
||||||
|
setHeadersList(
|
||||||
|
Object.entries(initialTool.headers || {}).map(([key, value], idx) => ({
|
||||||
|
id: `header-${idx}`,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
setBodyParams(
|
||||||
|
Object.entries(initialTool.parameters?.body_params || {}).map(([key, param], idx) => ({
|
||||||
|
id: `body-${idx}`,
|
||||||
|
key,
|
||||||
|
param,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
setPathParams(
|
||||||
|
Object.entries(initialTool.parameters?.path_params || {}).map(([key, desc], idx) => ({
|
||||||
|
id: `path-${idx}`,
|
||||||
|
key,
|
||||||
|
desc: desc as string,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
setQueryParams(
|
||||||
|
Object.entries(initialTool.parameters?.query_params || {}).map(([key, value], idx) => ({
|
||||||
|
id: `query-${idx}`,
|
||||||
|
key,
|
||||||
|
value: value as string,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
setValuesList(
|
||||||
|
Object.entries(initialTool.values || {}).map(([key, value], idx) => ({
|
||||||
|
id: `val-${idx}`,
|
||||||
|
key,
|
||||||
|
value: value as string,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
setTimeout(initialTool.error_handling?.timeout || 30);
|
||||||
|
setFallbackError(initialTool.error_handling?.fallback_response?.error || "");
|
||||||
|
setFallbackMessage(initialTool.error_handling?.fallback_response?.message || "");
|
||||||
|
} else {
|
||||||
|
setTool({
|
||||||
|
name: "",
|
||||||
|
method: "GET",
|
||||||
|
endpoint: "",
|
||||||
|
description: "",
|
||||||
|
headers: {},
|
||||||
|
values: {},
|
||||||
|
parameters: {},
|
||||||
|
error_handling: {
|
||||||
|
timeout: 30,
|
||||||
|
retry_count: 0,
|
||||||
|
fallback_response: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setHeadersList([]);
|
||||||
|
setBodyParams([]);
|
||||||
|
setPathParams([]);
|
||||||
|
setQueryParams([]);
|
||||||
|
setValuesList([]);
|
||||||
|
setTimeout(30);
|
||||||
|
setFallbackError("");
|
||||||
|
setFallbackMessage("");
|
||||||
|
}
|
||||||
|
setActiveTab("basics");
|
||||||
|
}
|
||||||
|
}, [open, initialTool]);
|
||||||
|
|
||||||
|
const handleAddHeader = () => {
|
||||||
|
setHeadersList([...headersList, { id: `header-${Date.now()}`, key: "", value: "" }]);
|
||||||
|
};
|
||||||
|
const handleRemoveHeader = (id: string) => {
|
||||||
|
setHeadersList(headersList.filter((h) => h.id !== id));
|
||||||
|
};
|
||||||
|
const handleHeaderChange = (id: string, field: "key" | "value", value: string) => {
|
||||||
|
setHeadersList(headersList.map((h) => (h.id === id ? { ...h, [field]: value } : h)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddBodyParam = () => {
|
||||||
|
setBodyParams([
|
||||||
|
...bodyParams,
|
||||||
|
{
|
||||||
|
id: `body-${Date.now()}`,
|
||||||
|
key: "",
|
||||||
|
param: { type: "string", required: false, description: "" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
const handleRemoveBodyParam = (id: string) => {
|
||||||
|
setBodyParams(bodyParams.filter((p) => p.id !== id));
|
||||||
|
};
|
||||||
|
const handleBodyParamChange = (id: string, field: "key" | keyof HTTPToolParameter, value: string | boolean) => {
|
||||||
|
setBodyParams(
|
||||||
|
bodyParams.map((p) =>
|
||||||
|
p.id === id
|
||||||
|
? field === "key"
|
||||||
|
? { ...p, key: value as string }
|
||||||
|
: { ...p, param: { ...p.param, [field]: value } }
|
||||||
|
: p
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Path Params
|
||||||
|
const handleAddPathParam = () => {
|
||||||
|
setPathParams([...pathParams, { id: `path-${Date.now()}`, key: "", desc: "" }]);
|
||||||
|
};
|
||||||
|
const handleRemovePathParam = (id: string) => {
|
||||||
|
setPathParams(pathParams.filter((p) => p.id !== id));
|
||||||
|
};
|
||||||
|
const handlePathParamChange = (id: string, field: "key" | "desc", value: string) => {
|
||||||
|
setPathParams(pathParams.map((p) => (p.id === id ? { ...p, [field]: value } : p)));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Query Params
|
||||||
|
const handleAddQueryParam = () => {
|
||||||
|
setQueryParams([...queryParams, { id: `query-${Date.now()}`, key: "", value: "" }]);
|
||||||
|
};
|
||||||
|
const handleRemoveQueryParam = (id: string) => {
|
||||||
|
setQueryParams(queryParams.filter((q) => q.id !== id));
|
||||||
|
};
|
||||||
|
const handleQueryParamChange = (id: string, field: "key" | "value", value: string) => {
|
||||||
|
setQueryParams(queryParams.map((q) => (q.id === id ? { ...q, [field]: value } : q)));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Values
|
||||||
|
const handleAddValue = () => {
|
||||||
|
setValuesList([...valuesList, { id: `val-${Date.now()}`, key: "", value: "" }]);
|
||||||
|
};
|
||||||
|
const handleRemoveValue = (id: string) => {
|
||||||
|
setValuesList(valuesList.filter((v) => v.id !== id));
|
||||||
|
};
|
||||||
|
const handleValueChange = (id: string, field: "key" | "value", value: string) => {
|
||||||
|
setValuesList(valuesList.map((v) => (v.id === id ? { ...v, [field]: value } : v)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!tool.name || !tool.endpoint) return;
|
||||||
|
const headersObject: Record<string, string> = {};
|
||||||
|
headersList.forEach((h) => {
|
||||||
|
if (h.key.trim()) headersObject[h.key] = h.value;
|
||||||
|
});
|
||||||
|
const bodyParamsObject: Record<string, HTTPToolParameter> = {};
|
||||||
|
bodyParams.forEach((p) => {
|
||||||
|
if (p.key.trim()) bodyParamsObject[p.key] = p.param;
|
||||||
|
});
|
||||||
|
const pathParamsObject: Record<string, string> = {};
|
||||||
|
pathParams.forEach((p) => {
|
||||||
|
if (p.key.trim()) pathParamsObject[p.key] = p.desc;
|
||||||
|
});
|
||||||
|
const queryParamsObject: Record<string, string> = {};
|
||||||
|
queryParams.forEach((q) => {
|
||||||
|
if (q.key.trim()) queryParamsObject[q.key] = q.value;
|
||||||
|
});
|
||||||
|
const valuesObject: Record<string, string> = {};
|
||||||
|
valuesList.forEach((v) => {
|
||||||
|
if (v.key.trim()) valuesObject[v.key] = v.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sanitize the tool name
|
||||||
|
const sanitizedName = sanitizeAgentName(tool.name);
|
||||||
|
|
||||||
|
onSave({
|
||||||
|
...(tool as HTTPTool),
|
||||||
|
name: sanitizedName,
|
||||||
|
headers: headersObject,
|
||||||
|
values: valuesObject,
|
||||||
|
parameters: {
|
||||||
|
...tool.parameters,
|
||||||
|
body_params: bodyParamsObject,
|
||||||
|
path_params: pathParamsObject,
|
||||||
|
query_params: queryParamsObject,
|
||||||
|
},
|
||||||
|
error_handling: {
|
||||||
|
timeout,
|
||||||
|
retry_count: tool.error_handling?.retry_count ?? 0,
|
||||||
|
fallback_response: {
|
||||||
|
error: fallbackError,
|
||||||
|
message: fallbackMessage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as HTTPTool);
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ParamField = ({
|
||||||
|
children,
|
||||||
|
label,
|
||||||
|
tooltip
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode,
|
||||||
|
label: string,
|
||||||
|
tooltip?: string
|
||||||
|
}) => (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Label className="text-sm font-medium text-neutral-200">
|
||||||
|
{label}
|
||||||
|
</Label>
|
||||||
|
{tooltip && (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Info className="h-3.5 w-3.5 text-neutral-400 cursor-help" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="bg-neutral-800 border-neutral-700 text-white p-3 max-w-sm">
|
||||||
|
<p className="text-xs">{tooltip}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const FieldList = <T extends Record<string, any>>({
|
||||||
|
items,
|
||||||
|
onAdd,
|
||||||
|
onRemove,
|
||||||
|
onChange,
|
||||||
|
fields,
|
||||||
|
addText,
|
||||||
|
emptyText,
|
||||||
|
icon
|
||||||
|
}: {
|
||||||
|
items: T[],
|
||||||
|
onAdd: () => void,
|
||||||
|
onRemove: (id: string) => void,
|
||||||
|
onChange: (id: string, field: string, value: any) => void,
|
||||||
|
fields: { name: string, field: string, placeholder: string, width: number, type?: string }[],
|
||||||
|
addText: string,
|
||||||
|
emptyText: string,
|
||||||
|
icon: React.ReactNode
|
||||||
|
}) => (
|
||||||
|
<div className="border border-neutral-700 rounded-md p-3 bg-neutral-800/50">
|
||||||
|
{items.length > 0 ? (
|
||||||
|
<div className="space-y-2 mb-3">
|
||||||
|
{items.map((item) => (
|
||||||
|
<div key={item.id} className="flex items-center gap-2 group">
|
||||||
|
<div className="flex-1 flex gap-2 w-full">
|
||||||
|
{fields.map(field => {
|
||||||
|
// Calculate percentage width based on the field's width value
|
||||||
|
const widthPercent = (field.width / 12) * 100;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={field.name}
|
||||||
|
className="flex-shrink-0"
|
||||||
|
style={{ width: `${widthPercent}%` }}
|
||||||
|
>
|
||||||
|
{field.type === 'select' ? (
|
||||||
|
<select
|
||||||
|
value={(field.field.includes('.')
|
||||||
|
? item.param?.[field.field.split('.')[1]]
|
||||||
|
: item[field.field]) || ''}
|
||||||
|
onChange={(e) => onChange(
|
||||||
|
item.id,
|
||||||
|
field.field,
|
||||||
|
e.target.value
|
||||||
|
)}
|
||||||
|
className="w-full h-9 px-3 py-1 rounded-md bg-neutral-900 border border-neutral-700 text-white text-sm"
|
||||||
|
>
|
||||||
|
<option value="string">string</option>
|
||||||
|
<option value="number">number</option>
|
||||||
|
<option value="boolean">boolean</option>
|
||||||
|
</select>
|
||||||
|
) : field.type === 'checkbox' ? (
|
||||||
|
<div className="flex items-center h-9 w-full justify-center">
|
||||||
|
<label className="flex items-center gap-1.5">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={item.param?.required || false}
|
||||||
|
onChange={(e) => onChange(
|
||||||
|
item.id,
|
||||||
|
field.field,
|
||||||
|
(e.target as HTMLInputElement).checked
|
||||||
|
)}
|
||||||
|
className="accent-emerald-400 rounded"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-neutral-300">Required</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
value={(field.field.includes('.')
|
||||||
|
? item.param?.[field.field.split('.')[1]]
|
||||||
|
: item[field.field]) || ''}
|
||||||
|
onChange={(e) => onChange(
|
||||||
|
item.id,
|
||||||
|
field.field,
|
||||||
|
e.target.value
|
||||||
|
)}
|
||||||
|
className="h-9 w-full bg-neutral-900 border-neutral-700 text-white placeholder:text-neutral-500 text-sm"
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => onRemove(item.id)}
|
||||||
|
className="h-9 w-9 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity text-red-400 hover:text-red-300 hover:bg-red-900/20"
|
||||||
|
>
|
||||||
|
<Trash className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="py-6 flex flex-col items-center justify-center text-center">
|
||||||
|
{icon}
|
||||||
|
<p className="mt-2 text-neutral-400 text-sm">{emptyText}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={onAdd}
|
||||||
|
className="w-full border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/10 bg-neutral-800/30"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-1.5" /> {addText}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-[850px] max-h-[90vh] overflow-hidden flex flex-col bg-neutral-900 border-neutral-700 p-0">
|
||||||
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
|
{/* Header Area */}
|
||||||
|
<div className="flex items-start justify-between p-6 border-b border-neutral-800">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold text-white flex items-center gap-2">
|
||||||
|
<Wand className="h-5 w-5 text-emerald-400" />
|
||||||
|
{initialTool ? "Edit Custom Tool" : "Create Custom Tool"}
|
||||||
|
</h2>
|
||||||
|
<p className="text-neutral-400 text-sm mt-1">
|
||||||
|
Configure an HTTP tool for your agent to interact with external APIs
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Badge variant="outline" className="bg-neutral-800 text-emerald-400 border-emerald-500/30 uppercase text-xs font-semibold px-2 py-0.5">
|
||||||
|
{tool.method || "GET"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content Area */}
|
||||||
|
<div className="flex-1 flex overflow-hidden">
|
||||||
|
{/* Left Side - Navigation */}
|
||||||
|
<div className="w-[200px] bg-neutral-800/50 border-r border-neutral-800 flex-shrink-0">
|
||||||
|
<div className="py-4">
|
||||||
|
<nav className="space-y-1 px-2">
|
||||||
|
{[
|
||||||
|
{ id: 'basics', label: 'Basic Info', icon: <Info className="h-4 w-4" /> },
|
||||||
|
{ id: 'endpoint', label: 'Endpoint', icon: <Globe className="h-4 w-4" /> },
|
||||||
|
{ id: 'headers', label: 'Headers', icon: <Server className="h-4 w-4" /> },
|
||||||
|
{ id: 'body', label: 'Body Params', icon: <FileJson className="h-4 w-4" /> },
|
||||||
|
{ id: 'path', label: 'Path Params', icon: <Code className="h-4 w-4" /> },
|
||||||
|
{ id: 'query', label: 'Query Params', icon: <LayoutList className="h-4 w-4" /> },
|
||||||
|
{ id: 'defaults', label: 'Default Values', icon: <Database className="h-4 w-4" /> },
|
||||||
|
{ id: 'error', label: 'Error Handling', icon: <Settings className="h-4 w-4" /> },
|
||||||
|
].map(item => (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
className={cn(
|
||||||
|
"w-full flex items-center gap-2 px-3 py-2 text-sm rounded-md transition-colors",
|
||||||
|
activeTab === item.id
|
||||||
|
? "bg-emerald-500/10 text-emerald-400"
|
||||||
|
: "text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800"
|
||||||
|
)}
|
||||||
|
onClick={() => setActiveTab(item.id)}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
<span>{item.label}</span>
|
||||||
|
{(
|
||||||
|
(item.id === 'headers' && headersList.length > 0) ||
|
||||||
|
(item.id === 'body' && bodyParams.length > 0) ||
|
||||||
|
(item.id === 'path' && pathParams.length > 0) ||
|
||||||
|
(item.id === 'query' && queryParams.length > 0) ||
|
||||||
|
(item.id === 'defaults' && valuesList.length > 0)
|
||||||
|
) && (
|
||||||
|
<span className="ml-auto bg-emerald-500/20 text-emerald-400 text-xs rounded-full px-1.5 py-0.5 min-w-[18px]">
|
||||||
|
{item.id === 'headers' && headersList.length}
|
||||||
|
{item.id === 'body' && bodyParams.length}
|
||||||
|
{item.id === 'path' && pathParams.length}
|
||||||
|
{item.id === 'query' && queryParams.length}
|
||||||
|
{item.id === 'defaults' && valuesList.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Side - Content */}
|
||||||
|
<div className="flex-1 overflow-auto">
|
||||||
|
{activeTab === 'basics' && (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<ParamField
|
||||||
|
label="Tool Name"
|
||||||
|
tooltip="A unique identifier for this tool. Will be used by the agent to reference this tool."
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={tool.name || ""}
|
||||||
|
onChange={(e) => setTool({ ...tool, name: e.target.value })}
|
||||||
|
onBlur={(e) => {
|
||||||
|
const sanitizedName = sanitizeAgentName(e.target.value);
|
||||||
|
if (sanitizedName !== e.target.value) {
|
||||||
|
setTool({ ...tool, name: sanitizedName });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="bg-neutral-800 border-neutral-700 text-white"
|
||||||
|
placeholder="e.g. weatherApi, searchTool"
|
||||||
|
/>
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField
|
||||||
|
label="Description"
|
||||||
|
tooltip="A clear description of what this tool does. The agent will use this to determine when to use the tool."
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={tool.description || ""}
|
||||||
|
onChange={(e) => setTool({ ...tool, description: e.target.value })}
|
||||||
|
className="bg-neutral-800 border-neutral-700 text-white"
|
||||||
|
placeholder="Provides weather information for a given location"
|
||||||
|
/>
|
||||||
|
</ParamField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'endpoint' && (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<ParamField
|
||||||
|
label="HTTP Method"
|
||||||
|
tooltip="The HTTP method to use for the request."
|
||||||
|
>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].map(method => (
|
||||||
|
<Button
|
||||||
|
key={method}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setTool({ ...tool, method })}
|
||||||
|
className={cn(
|
||||||
|
"px-4 py-2 rounded font-medium text-sm flex-1",
|
||||||
|
tool.method === method
|
||||||
|
? "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30"
|
||||||
|
: "bg-neutral-800 text-neutral-400 border border-neutral-700 hover:bg-neutral-700 hover:text-neutral-200"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{method}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField
|
||||||
|
label="Endpoint URL"
|
||||||
|
tooltip="The complete URL to call. Can include path parameters with the format {paramName}."
|
||||||
|
>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Input
|
||||||
|
value={tool.endpoint || ""}
|
||||||
|
onChange={(e) => setTool({ ...tool, endpoint: e.target.value })}
|
||||||
|
className="bg-neutral-800 border-neutral-700 text-white font-mono"
|
||||||
|
placeholder="https://api.example.com/v1/resource/{id}"
|
||||||
|
/>
|
||||||
|
{tool.endpoint && tool.endpoint.includes('{') && (
|
||||||
|
<p className="text-xs text-amber-400">
|
||||||
|
<Info className="h-3 w-3 inline-block mr-1" />
|
||||||
|
URL contains path variables. Don't forget to define them in the Path Parameters section.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<div className="p-4 bg-neutral-800/80 border border-emerald-600/20 rounded-md mt-6">
|
||||||
|
<h3 className="text-emerald-400 text-sm font-medium mb-2 flex items-center">
|
||||||
|
<Info className="h-4 w-4 mr-2" /> How to use variables in your endpoint
|
||||||
|
</h3>
|
||||||
|
<p className="text-neutral-300 text-sm mb-3">
|
||||||
|
You can use dynamic variables in your endpoint using the <code className="bg-neutral-700 px-1.5 py-0.5 rounded text-emerald-300">{"{VARIABLE_NAME}"}</code> syntax.
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 gap-4 text-xs">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-white mb-1">Example:</h4>
|
||||||
|
<code className="block bg-neutral-900 text-neutral-200 p-2 rounded">
|
||||||
|
https://api.example.com/users/<span className="text-emerald-400">{"{userId}"}</span>/profile
|
||||||
|
</code>
|
||||||
|
<p className="mt-1 text-neutral-400">Define <code className="bg-neutral-700 px-1 py-0.5 rounded text-emerald-300">userId</code> as a Path Parameter</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'headers' && (
|
||||||
|
<div className="p-6">
|
||||||
|
<ParamField
|
||||||
|
label="HTTP Headers"
|
||||||
|
tooltip="Headers to send with each request. Common examples include Authorization, Content-Type, Accept, etc."
|
||||||
|
>
|
||||||
|
<FieldList
|
||||||
|
items={headersList}
|
||||||
|
onAdd={handleAddHeader}
|
||||||
|
onRemove={handleRemoveHeader}
|
||||||
|
onChange={(id, field, value) => {
|
||||||
|
handleHeaderChange(
|
||||||
|
id,
|
||||||
|
field as "key" | "value",
|
||||||
|
value as string
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
fields={[
|
||||||
|
{ name: "Header Name", field: "key", placeholder: "e.g. Authorization", width: 6 },
|
||||||
|
{ name: "Header Value", field: "value", placeholder: "e.g. Bearer token123", width: 6 }
|
||||||
|
]}
|
||||||
|
addText="Add Header"
|
||||||
|
emptyText="No headers configured. Add headers to customize your HTTP requests."
|
||||||
|
icon={<Server className="h-10 w-10 text-neutral-700" />}
|
||||||
|
/>
|
||||||
|
</ParamField>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'body' && (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="mb-6 p-4 bg-neutral-800/80 border border-emerald-600/20 rounded-md">
|
||||||
|
<h3 className="text-emerald-400 text-sm font-medium mb-2 flex items-center">
|
||||||
|
<Info className="h-4 w-4 mr-2" /> About Body Parameters
|
||||||
|
</h3>
|
||||||
|
<p className="text-neutral-300 text-sm">
|
||||||
|
Parameters that will be sent in the request body. Only applicable for POST, PUT, and PATCH methods.
|
||||||
|
You can use variables like <code className="bg-neutral-700 px-1.5 py-0.5 rounded text-emerald-300">{"{variableName}"}</code> that will be replaced at runtime.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FieldList
|
||||||
|
items={bodyParams}
|
||||||
|
onAdd={handleAddBodyParam}
|
||||||
|
onRemove={handleRemoveBodyParam}
|
||||||
|
onChange={(id, field, value) => {
|
||||||
|
if (field === "key") {
|
||||||
|
handleBodyParamChange(id, "key", value as string);
|
||||||
|
} else if (field.startsWith("param.")) {
|
||||||
|
const paramField = field.split('.')[1] as keyof HTTPToolParameter;
|
||||||
|
handleBodyParamChange(id, paramField,
|
||||||
|
paramField === "required" ? value as boolean : value as string
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
fields={[
|
||||||
|
{ name: "Parameter Name", field: "key", placeholder: "e.g. userId", width: 4 },
|
||||||
|
{ name: "Type", field: "param.type", placeholder: "", width: 2, type: "select" },
|
||||||
|
{ name: "Description", field: "param.description", placeholder: "What this parameter does", width: 4 },
|
||||||
|
{ name: "Required", field: "param.required", placeholder: "", width: 2, type: "checkbox" }
|
||||||
|
]}
|
||||||
|
addText="Add Body Parameter"
|
||||||
|
emptyText="No body parameters configured. Add parameters for POST/PUT/PATCH requests."
|
||||||
|
icon={<FileJson className="h-10 w-10 text-neutral-700" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'path' && (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="mb-6 p-4 bg-neutral-800/80 border border-emerald-600/20 rounded-md">
|
||||||
|
<h3 className="text-emerald-400 text-sm font-medium mb-2 flex items-center">
|
||||||
|
<Info className="h-4 w-4 mr-2" /> About Path Parameters
|
||||||
|
</h3>
|
||||||
|
<p className="text-neutral-300 text-sm">
|
||||||
|
Path parameters are placeholders in your URL path that will be replaced with actual values.
|
||||||
|
For example, in <code className="bg-neutral-700 px-1.5 py-0.5 rounded text-emerald-300">https://api.example.com/users/{"{userId}"}</code>,
|
||||||
|
<code className="bg-neutral-700 px-1 py-0.5 rounded text-emerald-300">userId</code> is a path parameter.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FieldList
|
||||||
|
items={pathParams}
|
||||||
|
onAdd={handleAddPathParam}
|
||||||
|
onRemove={handleRemovePathParam}
|
||||||
|
onChange={(id, field, value) => {
|
||||||
|
handlePathParamChange(
|
||||||
|
id,
|
||||||
|
field as "key" | "desc",
|
||||||
|
value as string
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
fields={[
|
||||||
|
{ name: "Parameter Name", field: "key", placeholder: "e.g. userId", width: 4 },
|
||||||
|
{ name: "Description", field: "desc", placeholder: "What this parameter represents", width: 6 },
|
||||||
|
{ name: "Required", field: "required", placeholder: "", width: 2, type: "checkbox" }
|
||||||
|
]}
|
||||||
|
addText="Add Path Parameter"
|
||||||
|
emptyText="No path parameters configured. Add parameters if your endpoint URL contains {placeholders}."
|
||||||
|
icon={<Code className="h-10 w-10 text-neutral-700" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'query' && (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="mb-6 p-4 bg-neutral-800/80 border border-emerald-600/20 rounded-md">
|
||||||
|
<h3 className="text-emerald-400 text-sm font-medium mb-2 flex items-center">
|
||||||
|
<Info className="h-4 w-4 mr-2" /> About Query Parameters
|
||||||
|
</h3>
|
||||||
|
<p className="text-neutral-300 text-sm">
|
||||||
|
Query parameters are added to the URL after a question mark (?) to filter or customize the request.
|
||||||
|
For example: <code className="bg-neutral-700 px-1.5 py-0.5 rounded text-emerald-300">https://api.example.com/search?query=term&limit=10</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FieldList
|
||||||
|
items={queryParams}
|
||||||
|
onAdd={handleAddQueryParam}
|
||||||
|
onRemove={handleRemoveQueryParam}
|
||||||
|
onChange={(id, field, value) => {
|
||||||
|
handleQueryParamChange(
|
||||||
|
id,
|
||||||
|
field as "key" | "value",
|
||||||
|
value as string
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
fields={[
|
||||||
|
{ name: "Parameter Name", field: "key", placeholder: "e.g. search", width: 4 },
|
||||||
|
{ name: "Description", field: "value", placeholder: "What this parameter does", width: 6 },
|
||||||
|
{ name: "Required", field: "required", placeholder: "", width: 2, type: "checkbox" }
|
||||||
|
]}
|
||||||
|
addText="Add Query Parameter"
|
||||||
|
emptyText="No query parameters configured. Add parameters to customize your URL query string."
|
||||||
|
icon={<LayoutList className="h-10 w-10 text-neutral-700" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'defaults' && (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="mb-6 p-4 bg-neutral-800/80 border border-emerald-600/20 rounded-md">
|
||||||
|
<h3 className="text-emerald-400 text-sm font-medium mb-2 flex items-center">
|
||||||
|
<Info className="h-4 w-4 mr-2" /> About Default Values
|
||||||
|
</h3>
|
||||||
|
<p className="text-neutral-300 text-sm">
|
||||||
|
Set default values for parameters you've defined in the Body, Path, or Query sections.
|
||||||
|
These values will be used when the parameter isn't explicitly provided during a tool call.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FieldList
|
||||||
|
items={valuesList}
|
||||||
|
onAdd={handleAddValue}
|
||||||
|
onRemove={handleRemoveValue}
|
||||||
|
onChange={(id, field, value) => {
|
||||||
|
handleValueChange(
|
||||||
|
id,
|
||||||
|
field as "key" | "value",
|
||||||
|
value as string
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
fields={[
|
||||||
|
{ name: "Parameter Name", field: "key", placeholder: "Name of an existing parameter", width: 5 },
|
||||||
|
{ name: "Default Value", field: "value", placeholder: "Value to use if not provided", width: 7 }
|
||||||
|
]}
|
||||||
|
addText="Add Default Value"
|
||||||
|
emptyText="No default values configured. Add default values for your previously defined parameters."
|
||||||
|
icon={<Database className="h-10 w-10 text-neutral-700" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'error' && (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="mb-6 p-4 bg-neutral-800/80 border border-emerald-600/20 rounded-md">
|
||||||
|
<h3 className="text-emerald-400 text-sm font-medium mb-2 flex items-center">
|
||||||
|
<Info className="h-4 w-4 mr-2" /> About Error Handling
|
||||||
|
</h3>
|
||||||
|
<p className="text-neutral-300 text-sm">
|
||||||
|
Configure how the tool should handle errors and timeouts when making HTTP requests.
|
||||||
|
Proper error handling ensures reliable operation of your agent.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<ParamField
|
||||||
|
label="Timeout (seconds)"
|
||||||
|
tooltip="Maximum time to wait for a response before considering the request failed."
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={timeout}
|
||||||
|
onChange={(e) => setTimeout(Number(e.target.value))}
|
||||||
|
className="w-32 bg-neutral-800 border-neutral-700 text-white"
|
||||||
|
/>
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField
|
||||||
|
label="Fallback Error Code"
|
||||||
|
tooltip="Error code to return if the request fails."
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={fallbackError}
|
||||||
|
onChange={(e) => setFallbackError(e.target.value)}
|
||||||
|
className="bg-neutral-800 border-neutral-700 text-white"
|
||||||
|
placeholder="e.g. API_ERROR"
|
||||||
|
/>
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField
|
||||||
|
label="Fallback Error Message"
|
||||||
|
tooltip="Human-readable message to return if the request fails."
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={fallbackMessage}
|
||||||
|
onChange={(e) => setFallbackMessage(e.target.value)}
|
||||||
|
className="bg-neutral-800 border-neutral-700 text-white"
|
||||||
|
placeholder="e.g. Failed to retrieve data from the API."
|
||||||
|
/>
|
||||||
|
</ParamField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer Area */}
|
||||||
|
<div className="border-t border-neutral-800 p-4 flex items-center justify-between">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="text-neutral-400 hover:text-neutral-100 hover:bg-neutral-800"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="bg-emerald-500 text-neutral-950 hover:bg-emerald-400"
|
||||||
|
disabled={!tool.name || !tool.endpoint}
|
||||||
|
>
|
||||||
|
{initialTool ? "Save Changes" : "Create Tool"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
158
frontend/app/agents/dialogs/FolderDialog.tsx
Normal file
158
frontend/app/agents/dialogs/FolderDialog.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/dialogs/FolderDialog.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
interface Folder {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FolderDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSave: (folder: { name: string; description: string }) => Promise<void>;
|
||||||
|
editingFolder: Folder | null;
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FolderDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onSave,
|
||||||
|
editingFolder,
|
||||||
|
isLoading = false,
|
||||||
|
}: FolderDialogProps) {
|
||||||
|
const [folder, setFolder] = useState<{
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}>({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editingFolder) {
|
||||||
|
setFolder({
|
||||||
|
name: editingFolder.name,
|
||||||
|
description: editingFolder.description,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setFolder({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [editingFolder, open]);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
await onSave(folder);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-[500px] bg-[#1a1a1a] border-[#333] text-white">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{editingFolder ? "Edit Folder" : "New Folder"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-neutral-400">
|
||||||
|
{editingFolder
|
||||||
|
? "Update the existing folder information"
|
||||||
|
: "Fill in the information to create a new folder"}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="folder-name" className="text-neutral-300">
|
||||||
|
Folder Name
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="folder-name"
|
||||||
|
value={folder.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFolder({ ...folder, name: e.target.value })
|
||||||
|
}
|
||||||
|
className="bg-[#222] border-[#444] text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="folder-description" className="text-neutral-300">
|
||||||
|
Description (optional)
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="folder-description"
|
||||||
|
value={folder.description}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFolder({ ...folder, description: e.target.value })
|
||||||
|
}
|
||||||
|
className="bg-[#222] border-[#444] text-white resize-none h-20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||||
|
disabled={!folder.name || isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="animate-spin h-4 w-4 border-2 border-black border-t-transparent rounded-full mr-1"></div>
|
||||||
|
) : null}
|
||||||
|
{editingFolder ? "Save Changes" : "Create Folder"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
239
frontend/app/agents/dialogs/ImportAgentDialog.tsx
Normal file
239
frontend/app/agents/dialogs/ImportAgentDialog.tsx
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/dialogs/ImportAgentDialog.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 15, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useRef } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Loader2, Upload, FileJson } from "lucide-react";
|
||||||
|
import { importAgentFromJson } from "@/services/agentService";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
interface ImportAgentDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSuccess: () => void;
|
||||||
|
clientId: string;
|
||||||
|
folderId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImportAgentDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onSuccess,
|
||||||
|
clientId,
|
||||||
|
folderId,
|
||||||
|
}: ImportAgentDialogProps) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [dragActive, setDragActive] = useState(false);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const selectedFile = e.target.files?.[0];
|
||||||
|
if (selectedFile) {
|
||||||
|
setFile(selectedFile);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setDragActive(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setDragActive(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setDragActive(false);
|
||||||
|
|
||||||
|
const droppedFile = e.dataTransfer.files?.[0];
|
||||||
|
if (droppedFile && droppedFile.type === "application/json") {
|
||||||
|
setFile(droppedFile);
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "Invalid file",
|
||||||
|
description: "Please upload a JSON file",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectFile = () => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImport = async () => {
|
||||||
|
if (!file || !clientId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
await importAgentFromJson(file, clientId, folderId);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Import successful",
|
||||||
|
description: folderId
|
||||||
|
? "Agent was imported successfully and added to the current folder"
|
||||||
|
: "Agent was imported successfully",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call the success callback to refresh the agent list
|
||||||
|
onSuccess();
|
||||||
|
|
||||||
|
// Close the dialog
|
||||||
|
onOpenChange(false);
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
setFile(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error importing agent:", error);
|
||||||
|
toast({
|
||||||
|
title: "Import failed",
|
||||||
|
description: "There was an error importing the agent",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetState = () => {
|
||||||
|
if (!isLoading) {
|
||||||
|
setFile(null);
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(newOpen) => {
|
||||||
|
if (!newOpen) {
|
||||||
|
resetState();
|
||||||
|
}
|
||||||
|
onOpenChange(newOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent className="sm:max-w-[500px] bg-[#1a1a1a] border-[#333] text-white">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-white">Import Agent</DialogTitle>
|
||||||
|
<DialogDescription className="text-neutral-400">
|
||||||
|
Upload a JSON file to import an agent
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex flex-col space-y-4 py-4">
|
||||||
|
<div
|
||||||
|
className={`h-40 border-2 border-dashed rounded-md flex flex-col items-center justify-center p-4 transition-colors cursor-pointer ${
|
||||||
|
dragActive
|
||||||
|
? "border-emerald-400 bg-emerald-900/20"
|
||||||
|
: file
|
||||||
|
? "border-emerald-600 bg-emerald-900/10"
|
||||||
|
: "border-[#444] hover:border-emerald-600/50"
|
||||||
|
}`}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onClick={handleSelectFile}
|
||||||
|
>
|
||||||
|
{file ? (
|
||||||
|
<div className="flex flex-col items-center space-y-2">
|
||||||
|
<FileJson className="h-10 w-10 text-emerald-400" />
|
||||||
|
<span className="text-emerald-400 font-medium">{file.name}</span>
|
||||||
|
<span className="text-neutral-400 text-xs">
|
||||||
|
{Math.round(file.size / 1024)} KB
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center space-y-2">
|
||||||
|
<Upload className="h-10 w-10 text-neutral-500" />
|
||||||
|
<p className="text-neutral-400 text-center">
|
||||||
|
Drag & drop your JSON file here or click to browse
|
||||||
|
</p>
|
||||||
|
<span className="text-neutral-500 text-xs">
|
||||||
|
Only JSON files are accepted
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
accept=".json,application/json"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleImport}
|
||||||
|
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||||
|
disabled={!file || isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
Importing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Import Agent"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
278
frontend/app/agents/dialogs/MCPDialog.tsx
Normal file
278
frontend/app/agents/dialogs/MCPDialog.tsx
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/dialogs/MCPDialog.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { MCPServer } from "@/types/mcpServer";
|
||||||
|
import { Server } from "lucide-react";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
interface MCPDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSave: (mcpConfig: {
|
||||||
|
id: string;
|
||||||
|
envs: Record<string, string>;
|
||||||
|
tools: string[];
|
||||||
|
}) => void;
|
||||||
|
availableMCPs: MCPServer[];
|
||||||
|
selectedMCP?: MCPServer | null;
|
||||||
|
initialEnvs?: Record<string, string>;
|
||||||
|
initialTools?: string[];
|
||||||
|
clientId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MCPDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onSave,
|
||||||
|
availableMCPs,
|
||||||
|
selectedMCP: initialSelectedMCP = null,
|
||||||
|
initialEnvs = {},
|
||||||
|
initialTools = [],
|
||||||
|
clientId,
|
||||||
|
}: MCPDialogProps) {
|
||||||
|
const [selectedMCP, setSelectedMCP] = useState<MCPServer | null>(null);
|
||||||
|
const [mcpEnvs, setMcpEnvs] = useState<Record<string, string>>({});
|
||||||
|
const [selectedMCPTools, setSelectedMCPTools] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
if (initialSelectedMCP) {
|
||||||
|
setSelectedMCP(initialSelectedMCP);
|
||||||
|
setMcpEnvs(initialEnvs);
|
||||||
|
setSelectedMCPTools(initialTools);
|
||||||
|
} else {
|
||||||
|
setSelectedMCP(null);
|
||||||
|
setMcpEnvs({});
|
||||||
|
setSelectedMCPTools([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [open, initialSelectedMCP, initialEnvs, initialTools]);
|
||||||
|
|
||||||
|
const handleSelectMCP = (value: string) => {
|
||||||
|
const mcp = availableMCPs.find((m) => m.id === value);
|
||||||
|
if (mcp) {
|
||||||
|
setSelectedMCP(mcp);
|
||||||
|
const initialEnvs: Record<string, string> = {};
|
||||||
|
Object.keys(mcp.environments || {}).forEach((key) => {
|
||||||
|
initialEnvs[key] = "";
|
||||||
|
});
|
||||||
|
setMcpEnvs(initialEnvs);
|
||||||
|
setSelectedMCPTools([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleMCPTool = (tool: string) => {
|
||||||
|
if (selectedMCPTools.includes(tool)) {
|
||||||
|
setSelectedMCPTools(selectedMCPTools.filter((t) => t !== tool));
|
||||||
|
} else {
|
||||||
|
setSelectedMCPTools([...selectedMCPTools, tool]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!selectedMCP) return;
|
||||||
|
|
||||||
|
onSave({
|
||||||
|
id: selectedMCP.id,
|
||||||
|
envs: mcpEnvs,
|
||||||
|
tools: selectedMCPTools,
|
||||||
|
});
|
||||||
|
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-hidden flex flex-col bg-[#1a1a1a] border-[#333]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-white">
|
||||||
|
Configure MCP Server
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-neutral-400">
|
||||||
|
Select a MCP server and configure its tools.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-auto p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="mcp-select" className="text-neutral-300">
|
||||||
|
MCP Server
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedMCP?.id}
|
||||||
|
onValueChange={handleSelectMCP}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="bg-[#222] border-[#444] text-white">
|
||||||
|
<SelectValue placeholder="Select a MCP server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-[#222] border-[#444] text-white">
|
||||||
|
{availableMCPs.map((mcp) => (
|
||||||
|
<SelectItem
|
||||||
|
key={mcp.id}
|
||||||
|
value={mcp.id}
|
||||||
|
className="data-[selected]:bg-[#333] data-[highlighted]:bg-[#333] !text-white focus:!text-white hover:text-emerald-400 data-[selected]:!text-emerald-400"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Server className="h-4 w-4 text-emerald-400" />
|
||||||
|
{mcp.name}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedMCP && (
|
||||||
|
<>
|
||||||
|
<div className="border border-[#444] rounded-md p-3 bg-[#222]">
|
||||||
|
<p className="font-medium text-white">{selectedMCP.name}</p>
|
||||||
|
<p className="text-sm text-neutral-400">
|
||||||
|
{selectedMCP.description?.substring(0, 100)}...
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 text-xs text-neutral-400">
|
||||||
|
<p>
|
||||||
|
<strong>Type:</strong> {selectedMCP.type}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Configuration:</strong>{" "}
|
||||||
|
{selectedMCP.config_type === "sse" ? "SSE" : "Studio"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedMCP.environments &&
|
||||||
|
Object.keys(selectedMCP.environments).length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-sm font-medium text-white">
|
||||||
|
Environment Variables
|
||||||
|
</h3>
|
||||||
|
{Object.entries(selectedMCP.environments || {}).map(
|
||||||
|
([key, value]) => (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className="grid grid-cols-3 items-center gap-4"
|
||||||
|
>
|
||||||
|
<Label
|
||||||
|
htmlFor={`env-${key}`}
|
||||||
|
className="text-right text-neutral-300"
|
||||||
|
>
|
||||||
|
{key}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={`env-${key}`}
|
||||||
|
value={mcpEnvs[key] || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setMcpEnvs({
|
||||||
|
...mcpEnvs,
|
||||||
|
[key]: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="col-span-2 bg-[#222] border-[#444] text-white"
|
||||||
|
placeholder={value as string}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedMCP.tools && selectedMCP.tools.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-sm font-medium text-white">
|
||||||
|
Available Tools
|
||||||
|
</h3>
|
||||||
|
<div className="border border-[#444] rounded-md p-3 bg-[#222]">
|
||||||
|
{selectedMCP.tools.map((tool: any) => (
|
||||||
|
<div
|
||||||
|
key={tool.id}
|
||||||
|
className="flex items-center space-x-2 py-1"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={`tool-${tool.id}`}
|
||||||
|
checked={selectedMCPTools.includes(tool.id)}
|
||||||
|
onCheckedChange={() => toggleMCPTool(tool.id)}
|
||||||
|
className="data-[state=checked]:bg-emerald-400 data-[state=checked]:border-emerald-400"
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor={`tool-${tool.id}`}
|
||||||
|
className="text-sm text-neutral-300"
|
||||||
|
>
|
||||||
|
{tool.name}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="p-4 pt-2 border-t border-[#333]">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||||
|
disabled={!selectedMCP}
|
||||||
|
>
|
||||||
|
Add MCP
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
136
frontend/app/agents/dialogs/MoveAgentDialog.tsx
Normal file
136
frontend/app/agents/dialogs/MoveAgentDialog.tsx
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/dialogs/MoveAgentDialog.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Agent } from "@/types/agent";
|
||||||
|
import { Folder, Home } from "lucide-react";
|
||||||
|
|
||||||
|
interface Folder {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MoveAgentDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
agent: Agent | null;
|
||||||
|
folders: Folder[];
|
||||||
|
onMove: (folderId: string | null) => Promise<void>;
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MoveAgentDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
agent,
|
||||||
|
folders,
|
||||||
|
onMove,
|
||||||
|
isLoading = false,
|
||||||
|
}: MoveAgentDialogProps) {
|
||||||
|
const handleMove = async (folderId: string | null) => {
|
||||||
|
await onMove(folderId);
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-[500px] bg-[#1a1a1a] border-[#333] text-white">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Move Agent</DialogTitle>
|
||||||
|
<DialogDescription className="text-neutral-400">
|
||||||
|
Choose a folder to move the agent "{agent?.name}"
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<button
|
||||||
|
className="w-full text-left px-4 py-3 rounded-md flex items-center bg-[#222] border border-[#444] hover:bg-[#333] hover:border-emerald-400/50 transition-colors"
|
||||||
|
onClick={() => handleMove(null)}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<Home className="h-5 w-5 mr-3 text-neutral-400" />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">Remove from folder</div>
|
||||||
|
<p className="text-sm text-neutral-400">
|
||||||
|
The agent will be visible in "All agents"
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{folders.map((folder) => (
|
||||||
|
<button
|
||||||
|
key={folder.id}
|
||||||
|
className="w-full text-left px-4 py-3 rounded-md flex items-center bg-[#222] border border-[#444] hover:bg-[#333] hover:border-emerald-400/50 transition-colors"
|
||||||
|
onClick={() => handleMove(folder.id)}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<Folder className="h-5 w-5 mr-3 text-emerald-400" />
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{folder.name}</div>
|
||||||
|
{folder.description && (
|
||||||
|
<p className="text-sm text-neutral-400 truncate">
|
||||||
|
{folder.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
{isLoading && (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="animate-spin h-4 w-4 border-2 border-emerald-400 border-t-transparent rounded-full mr-2"></div>
|
||||||
|
<span className="text-neutral-400">Moving...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
172
frontend/app/agents/dialogs/ShareAgentDialog.tsx
Normal file
172
frontend/app/agents/dialogs/ShareAgentDialog.tsx
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/dialogs/ShareAgentDialog.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Agent } from "@/types/agent";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Copy, Share2, ExternalLink } from "lucide-react";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
interface ShareAgentDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
agent: Agent;
|
||||||
|
apiKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShareAgentDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
agent,
|
||||||
|
apiKey,
|
||||||
|
}: ShareAgentDialogProps) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [shareLink, setShareLink] = useState("");
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && agent && apiKey) {
|
||||||
|
const baseUrl = window.location.origin;
|
||||||
|
setShareLink(`${baseUrl}/shared-chat?agent=${agent.id}&key=${apiKey}`);
|
||||||
|
}
|
||||||
|
}, [open, agent, apiKey]);
|
||||||
|
|
||||||
|
const handleCopyLink = () => {
|
||||||
|
if (shareLink) {
|
||||||
|
navigator.clipboard.writeText(shareLink);
|
||||||
|
setCopied(true);
|
||||||
|
toast({
|
||||||
|
title: "Link copied!",
|
||||||
|
description: "The share link has been copied to the clipboard.",
|
||||||
|
});
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyApiKey = () => {
|
||||||
|
if (apiKey) {
|
||||||
|
navigator.clipboard.writeText(apiKey);
|
||||||
|
setCopied(true);
|
||||||
|
toast({
|
||||||
|
title: "API Key copied!",
|
||||||
|
description: "The API Key has been copied to the clipboard.",
|
||||||
|
});
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-[500px] bg-[#1a1a1a] border-[#333] text-white">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Share2 className="h-5 w-5 text-emerald-400" />
|
||||||
|
Share Agent
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-neutral-400">
|
||||||
|
Share this agent with others without the need to login.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-sm font-medium text-white">Share Link</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
readOnly
|
||||||
|
value={shareLink}
|
||||||
|
className="bg-[#222] border-[#444] text-white"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="shrink-0 bg-[#222] border-[#444] hover:bg-[#333] text-emerald-400 hover:text-emerald-300"
|
||||||
|
onClick={handleCopyLink}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-neutral-400">
|
||||||
|
Any person with this link can access the agent using the included API key.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-sm font-medium text-white">API Key</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
readOnly
|
||||||
|
value={apiKey}
|
||||||
|
type="password"
|
||||||
|
className="bg-[#222] border-[#444] text-white"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="shrink-0 bg-[#222] border-[#444] hover:bg-[#333] text-emerald-400 hover:text-emerald-300"
|
||||||
|
onClick={handleCopyApiKey}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-neutral-400">
|
||||||
|
The API key allows access to the agent. Do not share with untrusted people.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="border-t border-[#333] pt-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => window.open(shareLink, "_blank")}
|
||||||
|
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white flex gap-2"
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-4 w-4" />
|
||||||
|
Open Link
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
331
frontend/app/agents/forms/AgentForm.tsx
Normal file
331
frontend/app/agents/forms/AgentForm.tsx
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/forms/AgentForm.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { Agent } from "@/types/agent";
|
||||||
|
import { ApiKey } from "@/services/agentService";
|
||||||
|
import { MCPServer } from "@/types/mcpServer";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { BasicInfoTab } from "./BasicInfoTab";
|
||||||
|
import { ConfigurationTab } from "./ConfigurationTab";
|
||||||
|
import { SubAgentsTab } from "./SubAgentsTab";
|
||||||
|
import { MCPDialog } from "../dialogs/MCPDialog";
|
||||||
|
import { CustomMCPDialog } from "../dialogs/CustomMCPDialog";
|
||||||
|
|
||||||
|
interface ModelOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
provider: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AgentFormProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
initialValues: Partial<Agent>;
|
||||||
|
apiKeys: ApiKey[];
|
||||||
|
availableModels: ModelOption[];
|
||||||
|
availableMCPs: MCPServer[];
|
||||||
|
agents: Agent[];
|
||||||
|
onOpenApiKeysDialog: () => void;
|
||||||
|
onOpenMCPDialog: (mcp?: any) => void;
|
||||||
|
onOpenCustomMCPDialog: (customMCP?: any) => void;
|
||||||
|
onSave: (values: Partial<Agent>) => Promise<void>;
|
||||||
|
isLoading?: boolean;
|
||||||
|
getAgentNameById: (id: string) => string;
|
||||||
|
clientId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgentForm({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
initialValues,
|
||||||
|
apiKeys,
|
||||||
|
availableModels,
|
||||||
|
availableMCPs,
|
||||||
|
agents,
|
||||||
|
onOpenApiKeysDialog,
|
||||||
|
onOpenMCPDialog: externalOnOpenMCPDialog,
|
||||||
|
onOpenCustomMCPDialog: externalOnOpenCustomMCPDialog,
|
||||||
|
onSave,
|
||||||
|
isLoading = false,
|
||||||
|
getAgentNameById,
|
||||||
|
clientId,
|
||||||
|
}: AgentFormProps) {
|
||||||
|
const [values, setValues] = useState<Partial<Agent>>(initialValues);
|
||||||
|
const [activeTab, setActiveTab] = useState("basic");
|
||||||
|
|
||||||
|
const [mcpDialogOpen, setMcpDialogOpen] = useState(false);
|
||||||
|
const [selectedMCP, setSelectedMCP] = useState<any>(null);
|
||||||
|
const [customMcpDialogOpen, setCustomMcpDialogOpen] = useState(false);
|
||||||
|
const [selectedCustomMCP, setSelectedCustomMCP] = useState<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setValues(initialValues);
|
||||||
|
setActiveTab("basic");
|
||||||
|
}
|
||||||
|
}, [open, initialValues]);
|
||||||
|
|
||||||
|
const handleOpenMCPDialog = (mcpConfig: any = null) => {
|
||||||
|
setSelectedMCP(mcpConfig);
|
||||||
|
setMcpDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenCustomMCPDialog = (customMCP: any = null) => {
|
||||||
|
setSelectedCustomMCP(customMCP);
|
||||||
|
setCustomMcpDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfigureMCP = (mcpConfig: any) => {
|
||||||
|
handleOpenMCPDialog(mcpConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveMCP = (mcpId: string) => {
|
||||||
|
setValues({
|
||||||
|
...values,
|
||||||
|
config: {
|
||||||
|
...values.config,
|
||||||
|
mcp_servers:
|
||||||
|
values.config?.mcp_servers?.filter((mcp) => mcp.id !== mcpId) || [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfigureCustomMCP = (customMCP: any) => {
|
||||||
|
handleOpenCustomMCPDialog(customMCP);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveCustomMCP = (url: string) => {
|
||||||
|
setValues({
|
||||||
|
...values,
|
||||||
|
config: {
|
||||||
|
...values.config,
|
||||||
|
custom_mcp_servers:
|
||||||
|
values.config?.custom_mcp_servers?.filter(
|
||||||
|
(customMCP) => customMCP.url !== url
|
||||||
|
) || [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveMCP = (mcpConfig: any) => {
|
||||||
|
const updatedMcpServers = [...(values.config?.mcp_servers || [])];
|
||||||
|
const existingIndex = updatedMcpServers.findIndex(
|
||||||
|
(mcp) => mcp.id === mcpConfig.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
updatedMcpServers[existingIndex] = mcpConfig;
|
||||||
|
} else {
|
||||||
|
updatedMcpServers.push(mcpConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
setValues({
|
||||||
|
...values,
|
||||||
|
config: {
|
||||||
|
...(values.config || {}),
|
||||||
|
mcp_servers: updatedMcpServers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveCustomMCP = (customMCP: any) => {
|
||||||
|
const updatedCustomMCPs = [...(values.config?.custom_mcp_servers || [])];
|
||||||
|
const existingIndex = updatedCustomMCPs.findIndex(
|
||||||
|
(mcp) => mcp.url === customMCP.url
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
updatedCustomMCPs[existingIndex] = customMCP;
|
||||||
|
} else {
|
||||||
|
updatedCustomMCPs.push(customMCP);
|
||||||
|
}
|
||||||
|
|
||||||
|
setValues({
|
||||||
|
...values,
|
||||||
|
config: {
|
||||||
|
...(values.config || {}),
|
||||||
|
custom_mcp_servers: updatedCustomMCPs,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
const finalValues = {
|
||||||
|
...values,
|
||||||
|
client_id: clientId,
|
||||||
|
name: values.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
await onSave(finalValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-hidden flex flex-col bg-[#1a1a1a] border-[#333]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-white">
|
||||||
|
{initialValues.id ? "Edit Agent" : "New Agent"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-neutral-400">
|
||||||
|
{initialValues.id
|
||||||
|
? "Edit the existing agent information"
|
||||||
|
: "Fill in the information to create a new agent"}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onValueChange={setActiveTab}
|
||||||
|
className="flex-1 overflow-hidden flex flex-col"
|
||||||
|
>
|
||||||
|
<TabsList className="grid grid-cols-3 bg-[#222]">
|
||||||
|
<TabsTrigger
|
||||||
|
value="basic"
|
||||||
|
className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400"
|
||||||
|
>
|
||||||
|
Basic Information
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="config"
|
||||||
|
className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400"
|
||||||
|
>
|
||||||
|
Configuration
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="subagents"
|
||||||
|
className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400"
|
||||||
|
>
|
||||||
|
Sub-Agents
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<ScrollArea className="flex-1 overflow-auto">
|
||||||
|
<TabsContent value="basic" className="p-4 space-y-4">
|
||||||
|
<BasicInfoTab
|
||||||
|
values={values}
|
||||||
|
onChange={setValues}
|
||||||
|
apiKeys={apiKeys}
|
||||||
|
availableModels={availableModels}
|
||||||
|
onOpenApiKeysDialog={onOpenApiKeysDialog}
|
||||||
|
clientId={clientId}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="config" className="p-4 space-y-4">
|
||||||
|
<ConfigurationTab
|
||||||
|
values={values}
|
||||||
|
onChange={setValues}
|
||||||
|
agents={agents}
|
||||||
|
availableMCPs={availableMCPs}
|
||||||
|
apiKeys={apiKeys}
|
||||||
|
availableModels={availableModels}
|
||||||
|
getAgentNameById={getAgentNameById}
|
||||||
|
onOpenApiKeysDialog={onOpenApiKeysDialog}
|
||||||
|
onConfigureMCP={handleConfigureMCP}
|
||||||
|
onRemoveMCP={handleRemoveMCP}
|
||||||
|
onConfigureCustomMCP={handleConfigureCustomMCP}
|
||||||
|
onRemoveCustomMCP={handleRemoveCustomMCP}
|
||||||
|
onOpenMCPDialog={handleOpenMCPDialog}
|
||||||
|
onOpenCustomMCPDialog={handleOpenCustomMCPDialog}
|
||||||
|
clientId={clientId}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="subagents" className="p-4 space-y-4">
|
||||||
|
<SubAgentsTab
|
||||||
|
values={values}
|
||||||
|
onChange={setValues}
|
||||||
|
getAgentNameById={getAgentNameById}
|
||||||
|
editingAgentId={initialValues.id}
|
||||||
|
clientId={clientId}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
</ScrollArea>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||||
|
disabled={!values.name || isLoading}
|
||||||
|
>
|
||||||
|
{isLoading && (
|
||||||
|
<div className="animate-spin h-4 w-4 border-2 border-black border-t-transparent rounded-full mr-2"></div>
|
||||||
|
)}
|
||||||
|
{initialValues.id ? "Save Changes" : "Add Agent"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* MCP Dialog */}
|
||||||
|
<MCPDialog
|
||||||
|
open={mcpDialogOpen}
|
||||||
|
onOpenChange={setMcpDialogOpen}
|
||||||
|
onSave={handleSaveMCP}
|
||||||
|
availableMCPs={availableMCPs}
|
||||||
|
selectedMCP={
|
||||||
|
availableMCPs.find((m) => selectedMCP?.id === m.id) || null
|
||||||
|
}
|
||||||
|
initialEnvs={selectedMCP?.envs || {}}
|
||||||
|
initialTools={selectedMCP?.tools || []}
|
||||||
|
clientId={clientId}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Custom MCP Dialog */}
|
||||||
|
<CustomMCPDialog
|
||||||
|
open={customMcpDialogOpen}
|
||||||
|
onOpenChange={setCustomMcpDialogOpen}
|
||||||
|
onSave={handleSaveCustomMCP}
|
||||||
|
initialCustomMCP={selectedCustomMCP}
|
||||||
|
clientId={clientId}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
243
frontend/app/agents/forms/BasicInfoTab.tsx
Normal file
243
frontend/app/agents/forms/BasicInfoTab.tsx
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/forms/BasicInfoTab.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { AgentTypeSelector } from "@/app/agents/AgentTypeSelector";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Agent, AgentType } from "@/types/agent";
|
||||||
|
import { ApiKey } from "@/services/agentService";
|
||||||
|
import { A2AAgentConfig } from "../config/A2AAgentConfig";
|
||||||
|
import { LLMAgentConfig } from "../config/LLMAgentConfig";
|
||||||
|
import { sanitizeAgentName } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface ModelOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
provider: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BasicInfoTabProps {
|
||||||
|
values: Partial<Agent>;
|
||||||
|
onChange: (values: Partial<Agent>) => void;
|
||||||
|
apiKeys: ApiKey[];
|
||||||
|
availableModels: ModelOption[];
|
||||||
|
onOpenApiKeysDialog: () => void;
|
||||||
|
clientId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BasicInfoTab({
|
||||||
|
values,
|
||||||
|
onChange,
|
||||||
|
apiKeys,
|
||||||
|
availableModels,
|
||||||
|
onOpenApiKeysDialog,
|
||||||
|
}: BasicInfoTabProps) {
|
||||||
|
const handleNameBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
const sanitizedName = sanitizeAgentName(e.target.value);
|
||||||
|
if (sanitizedName !== e.target.value) {
|
||||||
|
onChange({ ...values, name: sanitizedName });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTypeChange = (type: AgentType) => {
|
||||||
|
let newValues: Partial<Agent> = { ...values, type };
|
||||||
|
|
||||||
|
if (type === "llm") {
|
||||||
|
newValues = {
|
||||||
|
...newValues,
|
||||||
|
model: "openai/gpt-4.1-nano",
|
||||||
|
instruction: "",
|
||||||
|
role: "",
|
||||||
|
goal: "",
|
||||||
|
agent_card_url: undefined,
|
||||||
|
config: {
|
||||||
|
tools: [],
|
||||||
|
mcp_servers: [],
|
||||||
|
custom_mcp_servers: [],
|
||||||
|
custom_tools: {
|
||||||
|
http_tools: [],
|
||||||
|
},
|
||||||
|
sub_agents: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (type === "a2a") {
|
||||||
|
newValues = {
|
||||||
|
...newValues,
|
||||||
|
model: undefined,
|
||||||
|
instruction: undefined,
|
||||||
|
role: undefined,
|
||||||
|
goal: undefined,
|
||||||
|
agent_card_url: "",
|
||||||
|
api_key_id: undefined,
|
||||||
|
config: undefined,
|
||||||
|
};
|
||||||
|
} else if (type === "loop") {
|
||||||
|
newValues = {
|
||||||
|
...newValues,
|
||||||
|
model: undefined,
|
||||||
|
instruction: undefined,
|
||||||
|
role: undefined,
|
||||||
|
goal: undefined,
|
||||||
|
agent_card_url: undefined,
|
||||||
|
api_key_id: undefined,
|
||||||
|
config: {
|
||||||
|
sub_agents: [],
|
||||||
|
custom_mcp_servers: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (type === "workflow") {
|
||||||
|
newValues = {
|
||||||
|
...newValues,
|
||||||
|
model: undefined,
|
||||||
|
instruction: undefined,
|
||||||
|
role: undefined,
|
||||||
|
goal: undefined,
|
||||||
|
agent_card_url: undefined,
|
||||||
|
api_key_id: undefined,
|
||||||
|
config: {
|
||||||
|
sub_agents: [],
|
||||||
|
workflow: {
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (type === "task") {
|
||||||
|
newValues = {
|
||||||
|
...newValues,
|
||||||
|
model: undefined,
|
||||||
|
instruction: undefined,
|
||||||
|
role: undefined,
|
||||||
|
goal: undefined,
|
||||||
|
agent_card_url: undefined,
|
||||||
|
api_key_id: undefined,
|
||||||
|
config: {
|
||||||
|
tasks: [],
|
||||||
|
sub_agents: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
newValues = {
|
||||||
|
...newValues,
|
||||||
|
model: undefined,
|
||||||
|
instruction: undefined,
|
||||||
|
role: undefined,
|
||||||
|
goal: undefined,
|
||||||
|
agent_card_url: undefined,
|
||||||
|
api_key_id: undefined,
|
||||||
|
config: {
|
||||||
|
sub_agents: [],
|
||||||
|
custom_mcp_servers: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(newValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="type" className="text-right text-neutral-300">
|
||||||
|
Agent Type
|
||||||
|
</Label>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<AgentTypeSelector
|
||||||
|
value={values.type || "llm"}
|
||||||
|
onValueChange={handleTypeChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="name" className="text-right text-neutral-300">
|
||||||
|
Name
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={values.name || ""}
|
||||||
|
onChange={(e) => onChange({ ...values, name: e.target.value })}
|
||||||
|
onBlur={handleNameBlur}
|
||||||
|
className="col-span-3 bg-[#222] border-[#444] text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{values.type !== "a2a" && (
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="description" className="text-right text-neutral-300">
|
||||||
|
Description
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="description"
|
||||||
|
value={values.description || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange({ ...values, description: e.target.value })
|
||||||
|
}
|
||||||
|
className="col-span-3 bg-[#222] border-[#444] text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{values.type === "llm" && (
|
||||||
|
<LLMAgentConfig
|
||||||
|
apiKeys={apiKeys}
|
||||||
|
availableModels={availableModels}
|
||||||
|
values={values}
|
||||||
|
onChange={onChange}
|
||||||
|
onOpenApiKeysDialog={onOpenApiKeysDialog}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{values.type === "loop" && values.config?.max_iterations && (
|
||||||
|
<div className="space-y-1 text-xs text-neutral-400">
|
||||||
|
<div>
|
||||||
|
<strong>Max. Iterations:</strong> {values.config.max_iterations}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{values.type === "workflow" && (
|
||||||
|
<div className="space-y-1 text-xs text-neutral-400">
|
||||||
|
<div>
|
||||||
|
<strong>Type:</strong> Visual Flow
|
||||||
|
</div>
|
||||||
|
{values.config?.workflow && (
|
||||||
|
<div>
|
||||||
|
<strong>Elements:</strong>{" "}
|
||||||
|
{values.config.workflow.nodes?.length || 0} nodes,{" "}
|
||||||
|
{values.config.workflow.edges?.length || 0} connections
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
722
frontend/app/agents/forms/ConfigurationTab.tsx
Normal file
722
frontend/app/agents/forms/ConfigurationTab.tsx
Normal file
@@ -0,0 +1,722 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/forms/ConfigurationTab.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Agent } from "@/types/agent";
|
||||||
|
import { MCPServer } from "@/types/mcpServer";
|
||||||
|
import {
|
||||||
|
Check,
|
||||||
|
Copy,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
Plus,
|
||||||
|
Server,
|
||||||
|
Settings,
|
||||||
|
X,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { ParallelAgentConfig } from "../config/ParallelAgentConfig";
|
||||||
|
import { SequentialAgentConfig } from "../config/SequentialAgentConfig";
|
||||||
|
import { ApiKey } from "@/services/agentService";
|
||||||
|
import { LoopAgentConfig } from "../config/LoopAgentConfig copy";
|
||||||
|
import { A2AAgentConfig } from "../config/A2AAgentConfig";
|
||||||
|
import { TaskAgentConfig } from "../config/TaskAgentConfig";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { MCPDialog } from "../dialogs/MCPDialog";
|
||||||
|
import { CustomMCPDialog } from "../dialogs/CustomMCPDialog";
|
||||||
|
import { AgentToolDialog } from "../dialogs/AgentToolDialog";
|
||||||
|
import { CustomToolDialog } from "../dialogs/CustomToolDialog";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
interface ConfigurationTabProps {
|
||||||
|
values: Partial<Agent>;
|
||||||
|
onChange: (values: Partial<Agent>) => void;
|
||||||
|
agents: Agent[];
|
||||||
|
availableMCPs: MCPServer[];
|
||||||
|
apiKeys: ApiKey[];
|
||||||
|
availableModels: any[];
|
||||||
|
getAgentNameById: (id: string) => string;
|
||||||
|
onOpenApiKeysDialog: () => void;
|
||||||
|
onConfigureMCP: (mcpConfig: any) => void;
|
||||||
|
onRemoveMCP: (mcpId: string) => void;
|
||||||
|
onConfigureCustomMCP: (customMCP: any) => void;
|
||||||
|
onRemoveCustomMCP: (url: string) => void;
|
||||||
|
onOpenMCPDialog: (mcpConfig?: any) => void;
|
||||||
|
onOpenCustomMCPDialog: (customMCP?: any) => void;
|
||||||
|
clientId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfigurationTab({
|
||||||
|
values,
|
||||||
|
onChange,
|
||||||
|
agents,
|
||||||
|
availableMCPs,
|
||||||
|
apiKeys,
|
||||||
|
availableModels,
|
||||||
|
getAgentNameById,
|
||||||
|
onOpenApiKeysDialog,
|
||||||
|
onConfigureMCP,
|
||||||
|
onRemoveMCP,
|
||||||
|
onConfigureCustomMCP,
|
||||||
|
onRemoveCustomMCP,
|
||||||
|
onOpenMCPDialog,
|
||||||
|
onOpenCustomMCPDialog,
|
||||||
|
clientId,
|
||||||
|
}: ConfigurationTabProps) {
|
||||||
|
const [agentToolDialogOpen, setAgentToolDialogOpen] = useState(false);
|
||||||
|
const [customToolDialogOpen, setCustomToolDialogOpen] = useState(false);
|
||||||
|
const [editingCustomTool, setEditingCustomTool] = useState<any>(null);
|
||||||
|
const [showApiKey, setShowApiKey] = useState(false);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [copiedCardUrl, setCopiedCardUrl] = useState(false);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const handleAddAgentTool = (tool: { id: string }) => {
|
||||||
|
const updatedAgentTools = [...(values.config?.agent_tools || [])];
|
||||||
|
if (!updatedAgentTools.includes(tool.id)) {
|
||||||
|
updatedAgentTools.push(tool.id);
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
config: {
|
||||||
|
...(values.config || {}),
|
||||||
|
agent_tools: updatedAgentTools,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleRemoveAgentTool = (id: string) => {
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
config: {
|
||||||
|
...(values.config || {}),
|
||||||
|
agent_tools: (values.config?.agent_tools || []).filter(
|
||||||
|
(toolId) => toolId !== id
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom Tools handlers
|
||||||
|
const handleAddCustomTool = (tool: any) => {
|
||||||
|
const updatedTools = [...(values.config?.custom_tools?.http_tools || [])];
|
||||||
|
updatedTools.push(tool);
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
config: {
|
||||||
|
...(values.config || {}),
|
||||||
|
custom_tools: {
|
||||||
|
...(values.config?.custom_tools || { http_tools: [] }),
|
||||||
|
http_tools: updatedTools,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleEditCustomTool = (tool: any, idx: number) => {
|
||||||
|
setEditingCustomTool({ ...tool, idx });
|
||||||
|
setCustomToolDialogOpen(true);
|
||||||
|
};
|
||||||
|
const handleSaveEditCustomTool = (tool: any) => {
|
||||||
|
const updatedTools = [...(values.config?.custom_tools?.http_tools || [])];
|
||||||
|
if (editingCustomTool && typeof editingCustomTool.idx === "number") {
|
||||||
|
updatedTools[editingCustomTool.idx] = tool;
|
||||||
|
}
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
config: {
|
||||||
|
...(values.config || {}),
|
||||||
|
custom_tools: {
|
||||||
|
...(values.config?.custom_tools || { http_tools: [] }),
|
||||||
|
http_tools: updatedTools,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setEditingCustomTool(null);
|
||||||
|
};
|
||||||
|
const handleRemoveCustomTool = (idx: number) => {
|
||||||
|
const updatedTools = [...(values.config?.custom_tools?.http_tools || [])];
|
||||||
|
updatedTools.splice(idx, 1);
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
config: {
|
||||||
|
...(values.config || {}),
|
||||||
|
custom_tools: {
|
||||||
|
...(values.config?.custom_tools || { http_tools: [] }),
|
||||||
|
http_tools: updatedTools,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiKeyField = (
|
||||||
|
<div className="space-y-2 mb-4">
|
||||||
|
<h3 className="text-lg font-medium text-white">Credentials</h3>
|
||||||
|
<div className="border border-[#444] rounded-md p-4 bg-[#222] flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label
|
||||||
|
className="text-sm text-neutral-400 mb-1"
|
||||||
|
htmlFor="agent-card-url"
|
||||||
|
>
|
||||||
|
Agent URL. This URL can be used to access the agent card externally.
|
||||||
|
</label>
|
||||||
|
<div className="relative flex items-center">
|
||||||
|
<input
|
||||||
|
id="agent-card-url"
|
||||||
|
type="text"
|
||||||
|
className="w-full bg-[#2a2a2a] border border-[#444] rounded-md px-3 py-2 text-white pr-12 focus:outline-none focus:ring-2 focus:ring-emerald-400/40"
|
||||||
|
value={
|
||||||
|
values?.agent_card_url?.replace(
|
||||||
|
"/.well-known/agent.json",
|
||||||
|
""
|
||||||
|
) || ""
|
||||||
|
}
|
||||||
|
disabled
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute right-2 text-neutral-400 hover:text-emerald-400 px-1 py-1"
|
||||||
|
onClick={async () => {
|
||||||
|
if (values?.agent_card_url) {
|
||||||
|
await navigator.clipboard.writeText(
|
||||||
|
values.agent_card_url.replace("/.well-known/agent.json", "")
|
||||||
|
);
|
||||||
|
setCopiedCardUrl(true);
|
||||||
|
setTimeout(() => setCopiedCardUrl(false), 1200);
|
||||||
|
toast({
|
||||||
|
title: "Copied!",
|
||||||
|
description:
|
||||||
|
"The agent URL was copied to the clipboard.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{copiedCardUrl ? (
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm text-neutral-400 mb-1" htmlFor="agent-api_key">
|
||||||
|
Configure the API Key for this agent. This key will be used for
|
||||||
|
authentication with external services.
|
||||||
|
</label>
|
||||||
|
<div className="relative flex items-center">
|
||||||
|
<input
|
||||||
|
id="agent-api_key"
|
||||||
|
type={showApiKey ? "text" : "password"}
|
||||||
|
className="w-full bg-[#2a2a2a] border border-[#444] rounded-md px-3 py-2 text-white pr-24 focus:outline-none focus:ring-2 focus:ring-emerald-400/40"
|
||||||
|
value={values.config?.api_key || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
config: {
|
||||||
|
...(values.config || {}),
|
||||||
|
api_key: e.target.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute right-9 text-neutral-400 hover:text-emerald-400 px-1 py-1"
|
||||||
|
onClick={() => setShowApiKey((v) => !v)}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{showApiKey ? (
|
||||||
|
<EyeOff className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute right-2 text-neutral-400 hover:text-emerald-400 px-1 py-1"
|
||||||
|
onClick={async () => {
|
||||||
|
if (values.config?.api_key) {
|
||||||
|
await navigator.clipboard.writeText(values.config.api_key);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 1200);
|
||||||
|
toast({
|
||||||
|
title: "Copied!",
|
||||||
|
description:
|
||||||
|
"The API key was copied to the clipboard.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (values.type === "llm") {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{apiKeyField}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium text-white">MCP Servers</h3>
|
||||||
|
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||||
|
<p className="text-sm text-neutral-400 mb-4">
|
||||||
|
Configure the MCP servers that this agent can use.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{values.config?.mcp_servers &&
|
||||||
|
values.config.mcp_servers.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{values.config.mcp_servers.map((mcpConfig) => {
|
||||||
|
const mcpServer = availableMCPs.find(
|
||||||
|
(mcp) => mcp.id === mcpConfig.id
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={mcpConfig.id}
|
||||||
|
className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-white">
|
||||||
|
{mcpServer?.name || mcpConfig.id}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-neutral-400">
|
||||||
|
{mcpServer?.description?.substring(0, 100)}
|
||||||
|
...
|
||||||
|
</p>
|
||||||
|
{mcpConfig.tools && mcpConfig.tools.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
|
{mcpConfig.tools.map((toolId) => (
|
||||||
|
<Badge
|
||||||
|
key={toolId}
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs bg-[#333] text-emerald-400 border-emerald-400/30"
|
||||||
|
>
|
||||||
|
{toolId}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onConfigureMCP(mcpConfig)}
|
||||||
|
className="flex items-center text-neutral-300 hover:text-emerald-400 hover:bg-[#333]"
|
||||||
|
>
|
||||||
|
<Settings className="h-4 w-4 mr-1" /> Configure
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onRemoveMCP(mcpConfig.id)}
|
||||||
|
className="text-red-500 hover:text-red-400 hover:bg-[#333]"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onOpenMCPDialog(null)}
|
||||||
|
className="w-full mt-2 border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-1" /> Add MCP Server
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md mb-2">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-white">
|
||||||
|
No MCP servers configured
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-neutral-400">
|
||||||
|
Add MCP servers for this agent
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onOpenMCPDialog(null)}
|
||||||
|
className="border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-1" /> Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium text-white">Custom MCPs</h3>
|
||||||
|
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||||
|
<p className="text-sm text-neutral-400 mb-4">
|
||||||
|
Configure custom MCPs with URL and HTTP headers.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{values.config?.custom_mcp_servers &&
|
||||||
|
values.config.custom_mcp_servers.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{values.config.custom_mcp_servers.map((customMCP) => (
|
||||||
|
<div
|
||||||
|
key={customMCP.url}
|
||||||
|
className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-white">{customMCP.url}</p>
|
||||||
|
<p className="text-sm text-neutral-400">
|
||||||
|
{Object.keys(customMCP.headers || {}).length > 0
|
||||||
|
? `${
|
||||||
|
Object.keys(customMCP.headers || {}).length
|
||||||
|
} headers configured`
|
||||||
|
: "No headers configured"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onConfigureCustomMCP(customMCP)}
|
||||||
|
className="flex items-center text-neutral-300 hover:text-emerald-400 hover:bg-[#333]"
|
||||||
|
>
|
||||||
|
<Settings className="h-4 w-4 mr-1" /> Configure
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onRemoveCustomMCP(customMCP.url)}
|
||||||
|
className="text-red-500 hover:text-red-400 hover:bg-[#333]"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onOpenCustomMCPDialog(null)}
|
||||||
|
className="w-full mt-2 border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-1" /> Add Custom MCP
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md mb-2">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-white">
|
||||||
|
No custom MCPs configured
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-neutral-400">
|
||||||
|
Add custom MCPs for this agent
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onOpenCustomMCPDialog(null)}
|
||||||
|
className="border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-1" /> Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium text-white">Agent Tools</h3>
|
||||||
|
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||||
|
<p className="text-sm text-neutral-400 mb-4">
|
||||||
|
Configure other agents as tools for this agent.
|
||||||
|
</p>
|
||||||
|
{values.config?.agent_tools &&
|
||||||
|
values.config.agent_tools.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{values.config.agent_tools.map((toolId) => {
|
||||||
|
const agent = agents.find((a) => a.id === toolId);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={toolId}
|
||||||
|
className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-white">
|
||||||
|
{agent?.name || toolId}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-neutral-400">
|
||||||
|
{agent?.description || "No description"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleRemoveAgentTool(toolId)}
|
||||||
|
className="text-red-500 hover:text-red-400 hover:bg-[#333]"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setAgentToolDialogOpen(true)}
|
||||||
|
className="w-full mt-2 border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-1" /> Add Agent Tool
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md mb-2">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-white">
|
||||||
|
No agent tools configured
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-neutral-400">
|
||||||
|
Add agent tools for this agent
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setAgentToolDialogOpen(true)}
|
||||||
|
className="border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-1" /> Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium text-white">
|
||||||
|
Custom Tools (HTTP Tools)
|
||||||
|
</h3>
|
||||||
|
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||||
|
<p className="text-sm text-neutral-400 mb-4">
|
||||||
|
Configure HTTP tools for this agent.
|
||||||
|
</p>
|
||||||
|
{values.config?.custom_tools?.http_tools &&
|
||||||
|
values.config.custom_tools.http_tools.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{values.config.custom_tools.http_tools.map((tool, idx) => (
|
||||||
|
<div
|
||||||
|
key={tool.name + idx}
|
||||||
|
className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-white">{tool.name}</p>
|
||||||
|
<p className="text-xs text-neutral-400">
|
||||||
|
{tool.method} {tool.endpoint}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-neutral-400">
|
||||||
|
{tool.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleEditCustomTool(tool, idx)}
|
||||||
|
className="flex items-center text-neutral-300 hover:text-emerald-400 hover:bg-[#333]"
|
||||||
|
>
|
||||||
|
<span className="mr-1">Edit</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleRemoveCustomTool(idx)}
|
||||||
|
className="text-red-500 hover:text-red-400 hover:bg-[#333]"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingCustomTool(null);
|
||||||
|
setCustomToolDialogOpen(true);
|
||||||
|
}}
|
||||||
|
className="w-full mt-2 border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-1" /> Add Custom Tool
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md mb-2">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-white">
|
||||||
|
No custom tools configured
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-neutral-400">
|
||||||
|
Add HTTP tools for this agent
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingCustomTool(null);
|
||||||
|
setCustomToolDialogOpen(true);
|
||||||
|
}}
|
||||||
|
className="border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-1" /> Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CustomToolDialog
|
||||||
|
open={customToolDialogOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setCustomToolDialogOpen(open);
|
||||||
|
if (!open) setEditingCustomTool(null);
|
||||||
|
}}
|
||||||
|
onSave={
|
||||||
|
editingCustomTool ? handleSaveEditCustomTool : handleAddCustomTool
|
||||||
|
}
|
||||||
|
initialTool={editingCustomTool}
|
||||||
|
/>
|
||||||
|
{agentToolDialogOpen && (
|
||||||
|
<AgentToolDialog
|
||||||
|
open={agentToolDialogOpen}
|
||||||
|
onOpenChange={setAgentToolDialogOpen}
|
||||||
|
onSave={handleAddAgentTool}
|
||||||
|
currentAgentId={values.id}
|
||||||
|
folderId={values.folder_id}
|
||||||
|
clientId={clientId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.type === "a2a") {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{apiKeyField}
|
||||||
|
<A2AAgentConfig values={values} onChange={onChange} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.type === "sequential") {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{apiKeyField}
|
||||||
|
<SequentialAgentConfig
|
||||||
|
values={values}
|
||||||
|
onChange={onChange}
|
||||||
|
agents={agents}
|
||||||
|
getAgentNameById={getAgentNameById}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.type === "parallel") {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{apiKeyField}
|
||||||
|
<ParallelAgentConfig
|
||||||
|
values={values}
|
||||||
|
onChange={onChange}
|
||||||
|
agents={agents}
|
||||||
|
getAgentNameById={getAgentNameById}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.type === "loop") {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{apiKeyField}
|
||||||
|
<LoopAgentConfig
|
||||||
|
values={values}
|
||||||
|
onChange={onChange}
|
||||||
|
agents={agents}
|
||||||
|
getAgentNameById={getAgentNameById}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.type === "task") {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{apiKeyField}
|
||||||
|
<TaskAgentConfig
|
||||||
|
values={values}
|
||||||
|
onChange={onChange}
|
||||||
|
agents={agents}
|
||||||
|
getAgentNameById={getAgentNameById}
|
||||||
|
singleTask={values.type === "task"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{apiKeyField}
|
||||||
|
<div className="flex items-center justify-center h-40">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-neutral-400">
|
||||||
|
Configure the sub-agents in the "Sub-Agents" tab
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
246
frontend/app/agents/forms/SubAgentsTab.tsx
Normal file
246
frontend/app/agents/forms/SubAgentsTab.tsx
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/forms/SubAgentsTab.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Agent } from "@/types/agent";
|
||||||
|
import { listAgents } from "@/services/agentService";
|
||||||
|
import { Loader2, Search, X } from "lucide-react";
|
||||||
|
|
||||||
|
interface SubAgentsTabProps {
|
||||||
|
values: Partial<Agent>;
|
||||||
|
onChange: (values: Partial<Agent>) => void;
|
||||||
|
getAgentNameById: (id: string) => string;
|
||||||
|
editingAgentId?: string;
|
||||||
|
clientId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SubAgentsTab({
|
||||||
|
values,
|
||||||
|
onChange,
|
||||||
|
getAgentNameById,
|
||||||
|
editingAgentId,
|
||||||
|
clientId,
|
||||||
|
}: SubAgentsTabProps) {
|
||||||
|
const [availableAgents, setAvailableAgents] = useState<Agent[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
// Get folder ID from current agent
|
||||||
|
const folderId = values.folder_id;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadAgents();
|
||||||
|
}, [clientId, folderId, editingAgentId]);
|
||||||
|
|
||||||
|
const loadAgents = async () => {
|
||||||
|
if (!clientId) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await listAgents(
|
||||||
|
clientId,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
folderId || undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter out the current agent to avoid self-reference
|
||||||
|
const filteredAgents = res.data.filter(agent =>
|
||||||
|
agent.id !== editingAgentId
|
||||||
|
);
|
||||||
|
|
||||||
|
setAvailableAgents(filteredAgents);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading agents:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddSubAgent = (agentId: string) => {
|
||||||
|
if (!values.config?.sub_agents?.includes(agentId)) {
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
config: {
|
||||||
|
...values.config,
|
||||||
|
sub_agents: [...(values.config?.sub_agents || []), agentId],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveSubAgent = (agentId: string) => {
|
||||||
|
onChange({
|
||||||
|
...values,
|
||||||
|
config: {
|
||||||
|
...values.config,
|
||||||
|
sub_agents:
|
||||||
|
values.config?.sub_agents?.filter((id) => id !== agentId) || [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredAgents = availableAgents.filter(agent =>
|
||||||
|
agent.name.toLowerCase().includes(search.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h3 className="text-lg font-medium text-white">Sub-Agents</h3>
|
||||||
|
<div className="text-sm text-neutral-400">
|
||||||
|
{values.config?.sub_agents?.length || 0} sub-agents selected
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||||
|
<p className="text-sm text-neutral-400 mb-4">
|
||||||
|
Select the agents that will be used as sub-agents.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{values.config?.sub_agents && values.config.sub_agents.length > 0 ? (
|
||||||
|
<div className="space-y-2 mb-4">
|
||||||
|
<h4 className="text-sm font-medium text-white">
|
||||||
|
Selected sub-agents:
|
||||||
|
</h4>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{values.config.sub_agents.map((agentId) => (
|
||||||
|
<Badge
|
||||||
|
key={agentId}
|
||||||
|
variant="secondary"
|
||||||
|
className="flex items-center gap-1 bg-[#333] text-emerald-400"
|
||||||
|
>
|
||||||
|
{getAgentNameById(agentId)}
|
||||||
|
<button
|
||||||
|
onClick={() => handleRemoveSubAgent(agentId)}
|
||||||
|
className="ml-1 h-4 w-4 rounded-full hover:bg-[#444] inline-flex items-center justify-center"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-4 text-neutral-400 mb-4">
|
||||||
|
No sub-agents selected
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<h4 className="text-sm font-medium text-white mb-2">
|
||||||
|
Available agents:
|
||||||
|
</h4>
|
||||||
|
<div className="relative mb-3">
|
||||||
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-neutral-500" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search agents by name..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
className="pl-9 bg-[#1a1a1a] border-[#444] text-white"
|
||||||
|
/>
|
||||||
|
{search && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSearch("")}
|
||||||
|
className="absolute right-2.5 top-2.5 text-neutral-400 hover:text-white"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-6">
|
||||||
|
<Loader2 className="h-6 w-6 text-emerald-400 animate-spin" />
|
||||||
|
<div className="mt-2 text-sm text-neutral-400">Loading agents...</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2 max-h-60 overflow-y-auto">
|
||||||
|
{filteredAgents.length === 0 ? (
|
||||||
|
<div className="text-center py-4 text-neutral-400">
|
||||||
|
{search ? `No agents found matching "${search}"` : "No other agents found in this folder"}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredAgents.map((agent) => (
|
||||||
|
<div
|
||||||
|
key={agent.id}
|
||||||
|
className="flex items-center justify-between p-2 hover:bg-[#2a2a2a] rounded-md"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium text-white">{agent.name}</span>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="ml-2 border-[#444] text-emerald-400"
|
||||||
|
>
|
||||||
|
{agent.type === "llm"
|
||||||
|
? "LLM Agent"
|
||||||
|
: agent.type === "a2a"
|
||||||
|
? "A2A Agent"
|
||||||
|
: agent.type === "sequential"
|
||||||
|
? "Sequential Agent"
|
||||||
|
: agent.type === "parallel"
|
||||||
|
? "Parallel Agent"
|
||||||
|
: agent.type === "loop"
|
||||||
|
? "Loop Agent"
|
||||||
|
: agent.type === "workflow"
|
||||||
|
? "Workflow Agent"
|
||||||
|
: agent.type === "task"
|
||||||
|
? "Task Agent"
|
||||||
|
: agent.type}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleAddSubAgent(agent.id)}
|
||||||
|
disabled={values.config?.sub_agents?.includes(agent.id)}
|
||||||
|
className={
|
||||||
|
values.config?.sub_agents?.includes(agent.id)
|
||||||
|
? "text-neutral-500 bg-[#222] hover:bg-[#333]"
|
||||||
|
: "text-emerald-400 hover:bg-[#333] bg-[#222]"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{values.config?.sub_agents?.includes(agent.id)
|
||||||
|
? "Added"
|
||||||
|
: "Add"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
754
frontend/app/agents/page.tsx
Normal file
754
frontend/app/agents/page.tsx
Normal file
@@ -0,0 +1,754 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/page.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Key, Plus, Folder, Download, Upload } from "lucide-react";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { exportAsJson } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
|
import { Agent, AgentCreate } from "@/types/agent";
|
||||||
|
import { Folder as AgentFolder } from "@/services/agentService";
|
||||||
|
import {
|
||||||
|
listAgents,
|
||||||
|
createAgent,
|
||||||
|
updateAgent,
|
||||||
|
deleteAgent,
|
||||||
|
listFolders,
|
||||||
|
createFolder,
|
||||||
|
updateFolder,
|
||||||
|
deleteFolder,
|
||||||
|
assignAgentToFolder,
|
||||||
|
ApiKey,
|
||||||
|
listApiKeys,
|
||||||
|
createApiKey,
|
||||||
|
updateApiKey,
|
||||||
|
deleteApiKey,
|
||||||
|
shareAgent,
|
||||||
|
importAgentFromJson,
|
||||||
|
} from "@/services/agentService";
|
||||||
|
import { listMCPServers } from "@/services/mcpServerService";
|
||||||
|
import { AgentSidebar } from "./AgentSidebar";
|
||||||
|
import { SearchInput } from "./SearchInput";
|
||||||
|
import { AgentList } from "./AgentList";
|
||||||
|
import { EmptyState } from "./EmptyState";
|
||||||
|
import { AgentForm } from "./forms/AgentForm";
|
||||||
|
import { FolderDialog } from "./dialogs/FolderDialog";
|
||||||
|
import { MoveAgentDialog } from "./dialogs/MoveAgentDialog";
|
||||||
|
import { ConfirmationDialog } from "./dialogs/ConfirmationDialog";
|
||||||
|
import { ApiKeysDialog } from "./dialogs/ApiKeysDialog";
|
||||||
|
import { ShareAgentDialog } from "./dialogs/ShareAgentDialog";
|
||||||
|
import { MCPServer } from "@/types/mcpServer";
|
||||||
|
import { availableModels } from "@/types/aiModels";
|
||||||
|
import { ImportAgentDialog } from "./dialogs/ImportAgentDialog";
|
||||||
|
|
||||||
|
export default function AgentsPage() {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const user =
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? JSON.parse(localStorage.getItem("user") || "{}")
|
||||||
|
: {};
|
||||||
|
const clientId = user?.client_id || "";
|
||||||
|
|
||||||
|
const [agents, setAgents] = useState<Agent[]>([]);
|
||||||
|
const [filteredAgents, setFilteredAgents] = useState<Agent[]>([]);
|
||||||
|
const [folders, setFolders] = useState<AgentFolder[]>([]);
|
||||||
|
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
|
||||||
|
const [availableMCPs, setAvailableMCPs] = useState<MCPServer[]>([]);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [selectedAgentType, setSelectedAgentType] = useState<string | null>(null);
|
||||||
|
const [agentTypes, setAgentTypes] = useState<string[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const [isSidebarVisible, setIsSidebarVisible] = useState(false);
|
||||||
|
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
const [isFolderDialogOpen, setIsFolderDialogOpen] = useState(false);
|
||||||
|
const [isMovingDialogOpen, setIsMovingDialogOpen] = useState(false);
|
||||||
|
const [isDeleteAgentDialogOpen, setIsDeleteAgentDialogOpen] = useState(false);
|
||||||
|
const [isDeleteFolderDialogOpen, setIsDeleteFolderDialogOpen] =
|
||||||
|
useState(false);
|
||||||
|
const [isApiKeysDialogOpen, setIsApiKeysDialogOpen] = useState(false);
|
||||||
|
const [isMCPDialogOpen, setIsMCPDialogOpen] = useState(false);
|
||||||
|
const [isCustomMCPDialogOpen, setIsCustomMCPDialogOpen] = useState(false);
|
||||||
|
const [isShareDialogOpen, setIsShareDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const [editingAgent, setEditingAgent] = useState<Agent | null>(null);
|
||||||
|
const [editingFolder, setEditingFolder] = useState<AgentFolder | null>(null);
|
||||||
|
const [agentToDelete, setAgentToDelete] = useState<Agent | null>(null);
|
||||||
|
const [agentToMove, setAgentToMove] = useState<Agent | null>(null);
|
||||||
|
const [agentToShare, setAgentToShare] = useState<Agent | null>(null);
|
||||||
|
const [sharedApiKey, setSharedApiKey] = useState<string>("");
|
||||||
|
const [folderToDelete, setFolderToDelete] = useState<AgentFolder | null>(null);
|
||||||
|
|
||||||
|
const [newAgent, setNewAgent] = useState<Partial<Agent>>({
|
||||||
|
client_id: clientId || "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
type: "llm",
|
||||||
|
model: "openai/gpt-4.1-nano",
|
||||||
|
instruction: "",
|
||||||
|
api_key_id: "",
|
||||||
|
config: {
|
||||||
|
tools: [],
|
||||||
|
mcp_servers: [],
|
||||||
|
custom_mcp_servers: [],
|
||||||
|
custom_tools: {
|
||||||
|
http_tools: [],
|
||||||
|
},
|
||||||
|
sub_agents: [],
|
||||||
|
agent_tools: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [isImporting, setIsImporting] = useState(false);
|
||||||
|
const [isImportDialogOpen, setIsImportDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!clientId) return;
|
||||||
|
loadAgents();
|
||||||
|
loadFolders();
|
||||||
|
loadApiKeys();
|
||||||
|
}, [clientId, selectedFolderId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadMCPs = async () => {
|
||||||
|
try {
|
||||||
|
const res = await listMCPServers();
|
||||||
|
setAvailableMCPs(res.data);
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error loading MCP servers",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadMCPs();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadAgents = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await listAgents(
|
||||||
|
clientId,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
selectedFolderId || undefined
|
||||||
|
);
|
||||||
|
setAgents(res.data);
|
||||||
|
setFilteredAgents(res.data);
|
||||||
|
|
||||||
|
// Extract unique agent types
|
||||||
|
const types = [...new Set(res.data.map(agent => agent.type))].filter(Boolean);
|
||||||
|
setAgentTypes(types);
|
||||||
|
} catch (error) {
|
||||||
|
toast({ title: "Error loading agents", variant: "destructive" });
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadFolders = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await listFolders(clientId);
|
||||||
|
setFolders(res.data);
|
||||||
|
} catch (error) {
|
||||||
|
toast({ title: "Error loading folders", variant: "destructive" });
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadApiKeys = async () => {
|
||||||
|
try {
|
||||||
|
const res = await listApiKeys(clientId);
|
||||||
|
setApiKeys(res.data);
|
||||||
|
} catch (error) {
|
||||||
|
toast({ title: "Error loading API keys", variant: "destructive" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Apply both search term and type filters
|
||||||
|
let filtered = [...agents];
|
||||||
|
|
||||||
|
// Apply search term filter
|
||||||
|
if (searchTerm.trim() !== "") {
|
||||||
|
const lowercaseSearch = searchTerm.toLowerCase();
|
||||||
|
filtered = filtered.filter(
|
||||||
|
(agent) =>
|
||||||
|
agent.name.toLowerCase().includes(lowercaseSearch) ||
|
||||||
|
agent.description?.toLowerCase().includes(lowercaseSearch)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply agent type filter
|
||||||
|
if (selectedAgentType) {
|
||||||
|
filtered = filtered.filter(agent => agent.type === selectedAgentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredAgents(filtered);
|
||||||
|
}, [searchTerm, selectedAgentType, agents]);
|
||||||
|
|
||||||
|
const handleAddAgent = async (agentData: Partial<Agent>) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
if (editingAgent) {
|
||||||
|
await updateAgent(editingAgent.id, {
|
||||||
|
...agentData,
|
||||||
|
client_id: clientId,
|
||||||
|
});
|
||||||
|
toast({
|
||||||
|
title: "Agent updated",
|
||||||
|
description: `${agentData.name} was updated successfully`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const createdAgent = await createAgent({
|
||||||
|
...(agentData as AgentCreate),
|
||||||
|
client_id: clientId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectedFolderId && createdAgent.data.id) {
|
||||||
|
await assignAgentToFolder(
|
||||||
|
createdAgent.data.id,
|
||||||
|
selectedFolderId,
|
||||||
|
clientId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Agent added",
|
||||||
|
description: `${agentData.name} was added successfully`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
loadAgents();
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
resetForm();
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Unable to save agent",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteAgent = async () => {
|
||||||
|
if (!agentToDelete) return;
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await deleteAgent(agentToDelete.id);
|
||||||
|
toast({
|
||||||
|
title: "Agent deleted",
|
||||||
|
description: "The agent was deleted successfully",
|
||||||
|
});
|
||||||
|
loadAgents();
|
||||||
|
setAgentToDelete(null);
|
||||||
|
setIsDeleteAgentDialogOpen(false);
|
||||||
|
} catch {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Unable to delete agent",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditAgent = (agent: Agent) => {
|
||||||
|
setEditingAgent(agent);
|
||||||
|
setNewAgent({ ...agent });
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMoveAgent = async (targetFolderId: string | null) => {
|
||||||
|
if (!agentToMove) return;
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await assignAgentToFolder(agentToMove.id, targetFolderId, clientId);
|
||||||
|
toast({
|
||||||
|
title: "Agent moved",
|
||||||
|
description: targetFolderId
|
||||||
|
? `Agent moved to folder successfully`
|
||||||
|
: "Agent removed from folder successfully",
|
||||||
|
});
|
||||||
|
setIsMovingDialogOpen(false);
|
||||||
|
loadAgents();
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Unable to move agent",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
setAgentToMove(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddFolder = async (folderData: {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
if (editingFolder) {
|
||||||
|
await updateFolder(editingFolder.id, folderData, clientId);
|
||||||
|
toast({
|
||||||
|
title: "Folder updated",
|
||||||
|
description: `${folderData.name} was updated successfully`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await createFolder({
|
||||||
|
...folderData,
|
||||||
|
client_id: clientId,
|
||||||
|
});
|
||||||
|
toast({
|
||||||
|
title: "Folder created",
|
||||||
|
description: `${folderData.name} was created successfully`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
loadFolders();
|
||||||
|
setIsFolderDialogOpen(false);
|
||||||
|
setEditingFolder(null);
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Unable to save folder",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteFolder = async () => {
|
||||||
|
if (!folderToDelete) return;
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await deleteFolder(folderToDelete.id, clientId);
|
||||||
|
toast({
|
||||||
|
title: "Folder deleted",
|
||||||
|
description: "The folder was deleted successfully",
|
||||||
|
});
|
||||||
|
loadFolders();
|
||||||
|
if (selectedFolderId === folderToDelete.id) {
|
||||||
|
setSelectedFolderId(null);
|
||||||
|
}
|
||||||
|
setFolderToDelete(null);
|
||||||
|
setIsDeleteFolderDialogOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Unable to delete folder",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShareAgent = async (agent: Agent) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setAgentToShare(agent);
|
||||||
|
const response = await shareAgent(agent.id, clientId);
|
||||||
|
|
||||||
|
if (response.data && response.data.api_key) {
|
||||||
|
setSharedApiKey(response.data.api_key);
|
||||||
|
setIsShareDialogOpen(true);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Agent shared",
|
||||||
|
description: "API key generated successfully",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Unable to share agent",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setNewAgent({
|
||||||
|
client_id: clientId || "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
type: "llm",
|
||||||
|
model: "openai/gpt-4.1-nano",
|
||||||
|
instruction: "",
|
||||||
|
api_key_id: "",
|
||||||
|
config: {
|
||||||
|
tools: [],
|
||||||
|
mcp_servers: [],
|
||||||
|
custom_mcp_servers: [],
|
||||||
|
custom_tools: {
|
||||||
|
http_tools: [],
|
||||||
|
},
|
||||||
|
sub_agents: [],
|
||||||
|
agent_tools: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setEditingAgent(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to export all agents as JSON
|
||||||
|
const handleExportAllAgents = () => {
|
||||||
|
try {
|
||||||
|
// Create file name with current date
|
||||||
|
const date = new Date();
|
||||||
|
const formattedDate = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
|
||||||
|
const filename = `agents-export-${formattedDate}`;
|
||||||
|
|
||||||
|
// Use the utility function to export
|
||||||
|
// Pass agents both as the data and as allAgents parameter to properly resolve references
|
||||||
|
const result = exportAsJson({ agents: filteredAgents }, filename, true, agents);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
toast({
|
||||||
|
title: "Export complete",
|
||||||
|
description: `${filteredAgents.length} agent(s) exported to JSON`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("Export failed");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error exporting agents:", error);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Export failed",
|
||||||
|
description: "There was an error exporting the agents",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImportAgentJSON = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (!file || !clientId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsImporting(true);
|
||||||
|
|
||||||
|
await importAgentFromJson(file, clientId);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Import successful",
|
||||||
|
description: "Agent was imported successfully",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh the agent list
|
||||||
|
loadAgents();
|
||||||
|
|
||||||
|
// Reset file input
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error importing agent:", error);
|
||||||
|
toast({
|
||||||
|
title: "Import failed",
|
||||||
|
description: "There was an error importing the agent",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsImporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6 bg-[#121212] min-h-screen flex relative">
|
||||||
|
<AgentSidebar
|
||||||
|
visible={isSidebarVisible}
|
||||||
|
folders={folders}
|
||||||
|
selectedFolderId={selectedFolderId}
|
||||||
|
onSelectFolder={setSelectedFolderId}
|
||||||
|
onAddFolder={() => {
|
||||||
|
setEditingFolder(null);
|
||||||
|
setIsFolderDialogOpen(true);
|
||||||
|
}}
|
||||||
|
onEditFolder={(folder) => {
|
||||||
|
setEditingFolder(folder as AgentFolder);
|
||||||
|
setIsFolderDialogOpen(true);
|
||||||
|
}}
|
||||||
|
onDeleteFolder={(folder) => {
|
||||||
|
setFolderToDelete(folder as AgentFolder);
|
||||||
|
setIsDeleteFolderDialogOpen(true);
|
||||||
|
}}
|
||||||
|
onClose={() => setIsSidebarVisible(!isSidebarVisible)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`w-full transition-all duration-300 ease-in-out ${
|
||||||
|
isSidebarVisible ? "pl-64" : "pl-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="mb-6 flex items-center justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{!isSidebarVisible && (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsSidebarVisible(true)}
|
||||||
|
className="mr-2 bg-[#222] p-2 rounded-md text-emerald-400 hover:bg-[#333] hover:text-emerald-400 shadow-md transition-all"
|
||||||
|
aria-label="Show folders"
|
||||||
|
>
|
||||||
|
<Folder className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<h1 className="text-3xl font-bold text-white flex items-center ml-2">
|
||||||
|
{selectedFolderId
|
||||||
|
? folders.find((f) => f.id === selectedFolderId)?.name
|
||||||
|
: "Agents"}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<SearchInput
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={setSearchTerm}
|
||||||
|
placeholder="Search agents..."
|
||||||
|
selectedAgentType={selectedAgentType}
|
||||||
|
onAgentTypeChange={setSelectedAgentType}
|
||||||
|
agentTypes={agentTypes}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsApiKeysDialogOpen(true)}
|
||||||
|
className="bg-[#222] text-white hover:bg-[#333] border border-[#444]"
|
||||||
|
>
|
||||||
|
<Key className="mr-2 h-4 w-4 text-emerald-400" />
|
||||||
|
API Keys
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleExportAllAgents}
|
||||||
|
className="bg-[#222] text-white hover:bg-[#333] border border-[#444]"
|
||||||
|
title="Export all agents as JSON"
|
||||||
|
>
|
||||||
|
<Download className="mr-2 h-4 w-4 text-purple-400" />
|
||||||
|
Export All
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button className="bg-emerald-400 text-black hover:bg-[#00cc7d]">
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
New Agent
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="bg-zinc-900 border-zinc-700">
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-white hover:bg-zinc-800 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
resetForm();
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-2 text-emerald-400" />
|
||||||
|
New Agent
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-white hover:bg-zinc-800 cursor-pointer"
|
||||||
|
onClick={() => setIsImportDialogOpen(true)}
|
||||||
|
>
|
||||||
|
<Upload className="h-4 w-4 mr-2 text-indigo-400" />
|
||||||
|
Import Agent JSON
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center h-[60vh]">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-emerald-400"></div>
|
||||||
|
</div>
|
||||||
|
) : filteredAgents.length > 0 ? (
|
||||||
|
<AgentList
|
||||||
|
agents={filteredAgents}
|
||||||
|
isLoading={isLoading}
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
selectedFolderId={selectedFolderId}
|
||||||
|
availableMCPs={availableMCPs}
|
||||||
|
getApiKeyNameById={(id) =>
|
||||||
|
apiKeys.find((k) => k.id === id)?.name || null
|
||||||
|
}
|
||||||
|
getAgentNameById={(id) =>
|
||||||
|
agents.find((a) => a.id === id)?.name || id
|
||||||
|
}
|
||||||
|
onEdit={handleEditAgent}
|
||||||
|
onDelete={(agent) => {
|
||||||
|
setAgentToDelete(agent);
|
||||||
|
setIsDeleteAgentDialogOpen(true);
|
||||||
|
}}
|
||||||
|
onMove={(agent) => {
|
||||||
|
setAgentToMove(agent);
|
||||||
|
setIsMovingDialogOpen(true);
|
||||||
|
}}
|
||||||
|
onShare={handleShareAgent}
|
||||||
|
onClearSearch={() => {
|
||||||
|
setSearchTerm("");
|
||||||
|
setSelectedAgentType(null);
|
||||||
|
}}
|
||||||
|
onCreateAgent={() => {
|
||||||
|
resetForm();
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
}}
|
||||||
|
onWorkflow={(agentId) => {
|
||||||
|
router.push(`/agents/workflows?agentId=${agentId}`);
|
||||||
|
}}
|
||||||
|
apiKeys={apiKeys}
|
||||||
|
folders={folders}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<EmptyState
|
||||||
|
type={
|
||||||
|
searchTerm || selectedAgentType
|
||||||
|
? "search-no-results"
|
||||||
|
: selectedFolderId
|
||||||
|
? "empty-folder"
|
||||||
|
: "no-agents"
|
||||||
|
}
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
onAction={() => {
|
||||||
|
searchTerm || selectedAgentType
|
||||||
|
? (setSearchTerm(""), setSelectedAgentType(null))
|
||||||
|
: (resetForm(), setIsDialogOpen(true));
|
||||||
|
}}
|
||||||
|
actionLabel={searchTerm || selectedAgentType ? "Clear filters" : "Create Agent"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AgentForm
|
||||||
|
open={isDialogOpen}
|
||||||
|
onOpenChange={setIsDialogOpen}
|
||||||
|
initialValues={newAgent}
|
||||||
|
apiKeys={apiKeys}
|
||||||
|
availableModels={availableModels}
|
||||||
|
availableMCPs={availableMCPs}
|
||||||
|
agents={agents}
|
||||||
|
onOpenApiKeysDialog={() => setIsApiKeysDialogOpen(true)}
|
||||||
|
onOpenMCPDialog={() => setIsMCPDialogOpen(true)}
|
||||||
|
onOpenCustomMCPDialog={() => setIsCustomMCPDialogOpen(true)}
|
||||||
|
onSave={handleAddAgent}
|
||||||
|
getAgentNameById={(id) => agents.find((a) => a.id === id)?.name || id}
|
||||||
|
clientId={clientId}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FolderDialog
|
||||||
|
open={isFolderDialogOpen}
|
||||||
|
onOpenChange={setIsFolderDialogOpen}
|
||||||
|
editingFolder={editingFolder}
|
||||||
|
onSave={handleAddFolder}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MoveAgentDialog
|
||||||
|
open={isMovingDialogOpen}
|
||||||
|
onOpenChange={setIsMovingDialogOpen}
|
||||||
|
agent={agentToMove}
|
||||||
|
folders={folders}
|
||||||
|
onMove={handleMoveAgent}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmationDialog
|
||||||
|
open={isDeleteAgentDialogOpen}
|
||||||
|
onOpenChange={setIsDeleteAgentDialogOpen}
|
||||||
|
title="Confirm deletion"
|
||||||
|
description={`Are you sure you want to delete the agent "${agentToDelete?.name}"? This action cannot be undone.`}
|
||||||
|
onConfirm={handleDeleteAgent}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmationDialog
|
||||||
|
open={isDeleteFolderDialogOpen}
|
||||||
|
onOpenChange={setIsDeleteFolderDialogOpen}
|
||||||
|
title="Confirm deletion"
|
||||||
|
description={`Are you sure you want to delete the folder "${folderToDelete?.name}"? This action cannot be undone.`}
|
||||||
|
confirmText="Delete"
|
||||||
|
confirmVariant="destructive"
|
||||||
|
onConfirm={handleDeleteFolder}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ApiKeysDialog
|
||||||
|
open={isApiKeysDialogOpen}
|
||||||
|
onOpenChange={setIsApiKeysDialogOpen}
|
||||||
|
apiKeys={apiKeys}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onAddApiKey={async (keyData) => {
|
||||||
|
await createApiKey({ ...keyData, client_id: clientId });
|
||||||
|
loadApiKeys();
|
||||||
|
}}
|
||||||
|
onUpdateApiKey={async (id, keyData) => {
|
||||||
|
await updateApiKey(id, keyData, clientId);
|
||||||
|
loadApiKeys();
|
||||||
|
}}
|
||||||
|
onDeleteApiKey={async (id) => {
|
||||||
|
await deleteApiKey(id, clientId);
|
||||||
|
loadApiKeys();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ShareAgentDialog
|
||||||
|
open={isShareDialogOpen}
|
||||||
|
onOpenChange={setIsShareDialogOpen}
|
||||||
|
agent={agentToShare || ({} as Agent)}
|
||||||
|
apiKey={sharedApiKey}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ImportAgentDialog
|
||||||
|
open={isImportDialogOpen}
|
||||||
|
onOpenChange={setIsImportDialogOpen}
|
||||||
|
onSuccess={loadAgents}
|
||||||
|
clientId={clientId}
|
||||||
|
folderId={selectedFolderId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
689
frontend/app/agents/workflows/Canva.tsx
Normal file
689
frontend/app/agents/workflows/Canva.tsx
Normal file
@@ -0,0 +1,689 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/Canva.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Delay node integration developed by: Victor Calazans │
|
||||||
|
│ Creation date: May 13, 2025 |
|
||||||
|
│ Delay implementation date: May 17, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Controls,
|
||||||
|
ReactFlow,
|
||||||
|
addEdge,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
type OnConnect,
|
||||||
|
ConnectionMode,
|
||||||
|
ConnectionLineType,
|
||||||
|
useReactFlow,
|
||||||
|
ProOptions,
|
||||||
|
applyNodeChanges,
|
||||||
|
NodeChange,
|
||||||
|
OnNodesChange,
|
||||||
|
MiniMap,
|
||||||
|
Panel,
|
||||||
|
Background,
|
||||||
|
} from "@xyflow/react";
|
||||||
|
import { useDnD } from "@/contexts/DnDContext";
|
||||||
|
|
||||||
|
import { Edit, X, ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
|
||||||
|
import "@xyflow/react/dist/style.css";
|
||||||
|
import "./canva.css";
|
||||||
|
|
||||||
|
import { getHelperLines } from "./utils";
|
||||||
|
|
||||||
|
import { NodePanel } from "./NodePanel";
|
||||||
|
import ContextMenu from "./ContextMenu";
|
||||||
|
import { initialEdges, edgeTypes } from "./edges";
|
||||||
|
import HelperLines from "./HelperLines";
|
||||||
|
import { initialNodes, nodeTypes } from "./nodes";
|
||||||
|
import { AgentForm } from "./nodes/components/agent/AgentForm";
|
||||||
|
import { ConditionForm } from "./nodes/components/condition/ConditionForm";
|
||||||
|
import { Agent, WorkflowData } from "@/types/agent";
|
||||||
|
import { updateAgent } from "@/services/agentService";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { MessageForm } from "./nodes/components/message/MessageForm";
|
||||||
|
import { DelayForm } from "./nodes/components/delay/DelayForm";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
const proOptions: ProOptions = { account: "paid-pro", hideAttribution: true };
|
||||||
|
|
||||||
|
const NodeFormWrapper = ({
|
||||||
|
selectedNode,
|
||||||
|
editingLabel,
|
||||||
|
setEditingLabel,
|
||||||
|
handleUpdateNode,
|
||||||
|
setSelectedNode,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
selectedNode: any;
|
||||||
|
editingLabel: boolean;
|
||||||
|
setEditingLabel: (value: boolean) => void;
|
||||||
|
handleUpdateNode: (node: any) => void;
|
||||||
|
setSelectedNode: (node: any) => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
// Handle ESC key to close the panel
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape" && !editingLabel) {
|
||||||
|
setSelectedNode(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [setSelectedNode, editingLabel]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="flex-shrink-0 sticky top-0 z-20 bg-neutral-800 shadow-md border-b border-neutral-700">
|
||||||
|
<div className="p-4 text-center relative">
|
||||||
|
<button
|
||||||
|
className="absolute right-2 top-2 text-neutral-200 hover:text-white p-1 rounded-full hover:bg-neutral-700"
|
||||||
|
onClick={() => setSelectedNode(null)}
|
||||||
|
aria-label="Close panel"
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
{!editingLabel ? (
|
||||||
|
<div className="flex items-center justify-center text-xl font-bold text-neutral-200">
|
||||||
|
<span>{selectedNode.data.label}</span>
|
||||||
|
{selectedNode.type !== "start-node" && (
|
||||||
|
<Edit
|
||||||
|
size={16}
|
||||||
|
className="ml-2 cursor-pointer hover:text-indigo-300"
|
||||||
|
onClick={() => setEditingLabel(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={selectedNode.data.label}
|
||||||
|
className="w-full p-2 text-center text-xl font-bold bg-neutral-800 text-neutral-200 border border-neutral-600 rounded"
|
||||||
|
onChange={(e) => {
|
||||||
|
handleUpdateNode({
|
||||||
|
...selectedNode,
|
||||||
|
data: {
|
||||||
|
...selectedNode.data,
|
||||||
|
label: e.target.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
setEditingLabel(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={() => setEditingLabel(false)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-h-0 overflow-hidden">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Canva = forwardRef(({ agent }: { agent: Agent | null }, ref) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [nodes, setNodes] = useNodesState(initialNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
const { screenToFlowPosition } = useReactFlow();
|
||||||
|
const { type, setPointerEvents } = useDnD();
|
||||||
|
const [menu, setMenu] = useState<any>(null);
|
||||||
|
const localRef = useRef<any>(null);
|
||||||
|
const [selectedNode, setSelectedNode] = useState<any>(null);
|
||||||
|
const [activeExecutionNodeId, setActiveExecutionNodeId] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
const [editingLabel, setEditingLabel] = useState(false);
|
||||||
|
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
|
||||||
|
const [nodePanelOpen, setNodePanelOpen] = useState(false);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
getFlowData: () => ({
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
}),
|
||||||
|
setHasChanges,
|
||||||
|
setActiveExecutionNodeId,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Effect to clear the active node after a timeout
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeExecutionNodeId) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setActiveExecutionNodeId(null);
|
||||||
|
}, 5000); // Increase to 5 seconds to give more time to visualize
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [activeExecutionNodeId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
agent?.config?.workflow &&
|
||||||
|
agent.config.workflow.nodes.length > 0 &&
|
||||||
|
agent.config.workflow.edges.length > 0
|
||||||
|
) {
|
||||||
|
setNodes(
|
||||||
|
(agent.config.workflow.nodes as typeof initialNodes) || initialNodes
|
||||||
|
);
|
||||||
|
setEdges(
|
||||||
|
(agent.config.workflow.edges as typeof initialEdges) || initialEdges
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setNodes(initialNodes);
|
||||||
|
setEdges(initialEdges);
|
||||||
|
}
|
||||||
|
}, [agent, setNodes, setEdges]);
|
||||||
|
|
||||||
|
// Update nodes when the active node changes to add visual class
|
||||||
|
useEffect(() => {
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
setNodes((nds: any) =>
|
||||||
|
nds.map((node: any) => {
|
||||||
|
if (node.id === activeExecutionNodeId) {
|
||||||
|
// Add a class to highlight the active node
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
className: "active-execution-node",
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
isExecuting: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Remove the highlight class
|
||||||
|
const { isExecuting, ...restData } = node.data || {};
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
className: "",
|
||||||
|
data: restData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [activeExecutionNodeId, setNodes]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (agent?.config?.workflow) {
|
||||||
|
const initialNodes = agent.config.workflow.nodes || [];
|
||||||
|
const initialEdges = agent.config.workflow.edges || [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
JSON.stringify(nodes) !== JSON.stringify(initialNodes) ||
|
||||||
|
JSON.stringify(edges) !== JSON.stringify(initialEdges)
|
||||||
|
) {
|
||||||
|
setHasChanges(true);
|
||||||
|
} else {
|
||||||
|
setHasChanges(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [nodes, edges, agent]);
|
||||||
|
|
||||||
|
const [helperLineHorizontal, setHelperLineHorizontal] = useState<
|
||||||
|
number | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [helperLineVertical, setHelperLineVertical] = useState<
|
||||||
|
number | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
const onConnect: OnConnect = useCallback(
|
||||||
|
(connection) => {
|
||||||
|
setEdges((currentEdges) => {
|
||||||
|
if (connection.source === connection.target) {
|
||||||
|
return currentEdges;
|
||||||
|
}
|
||||||
|
|
||||||
|
return addEdge(connection, currentEdges);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setEdges]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onConnectEnd = useCallback(
|
||||||
|
(_event: any, connectionState: any) => {
|
||||||
|
setPointerEvents("none");
|
||||||
|
|
||||||
|
if (connectionState.fromHandle?.type === "target") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connectionState.isValid) {
|
||||||
|
// Since we're using NodePanel now, we don't need to do anything here
|
||||||
|
// The panel will handle node creation through drag and drop
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setPointerEvents]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onConnectStart = useCallback(() => {
|
||||||
|
setPointerEvents("auto");
|
||||||
|
}, [setPointerEvents]);
|
||||||
|
|
||||||
|
const customApplyNodeChanges = useCallback(
|
||||||
|
(changes: NodeChange[], nodes: any): any => {
|
||||||
|
// reset the helper lines (clear existing lines, if any)
|
||||||
|
setHelperLineHorizontal(undefined);
|
||||||
|
setHelperLineVertical(undefined);
|
||||||
|
|
||||||
|
// this will be true if it's a single node being dragged
|
||||||
|
// inside we calculate the helper lines and snap position for the position where the node is being moved to
|
||||||
|
if (
|
||||||
|
changes.length === 1 &&
|
||||||
|
changes[0].type === "position" &&
|
||||||
|
changes[0].dragging &&
|
||||||
|
changes[0].position
|
||||||
|
) {
|
||||||
|
const helperLines = getHelperLines(changes[0], nodes);
|
||||||
|
|
||||||
|
// if we have a helper line, we snap the node to the helper line position
|
||||||
|
// this is being done by manipulating the node position inside the change object
|
||||||
|
changes[0].position.x =
|
||||||
|
helperLines.snapPosition.x ?? changes[0].position.x;
|
||||||
|
changes[0].position.y =
|
||||||
|
helperLines.snapPosition.y ?? changes[0].position.y;
|
||||||
|
|
||||||
|
// if helper lines are returned, we set them so that they can be displayed
|
||||||
|
setHelperLineHorizontal(helperLines.horizontal);
|
||||||
|
setHelperLineVertical(helperLines.vertical);
|
||||||
|
}
|
||||||
|
|
||||||
|
return applyNodeChanges(changes, nodes);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onNodesChange: OnNodesChange = useCallback(
|
||||||
|
(changes) => {
|
||||||
|
setNodes((nodes) => customApplyNodeChanges(changes, nodes));
|
||||||
|
},
|
||||||
|
[setNodes, customApplyNodeChanges]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getLabelFromNode = (type: string) => {
|
||||||
|
const order = nodes.length;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "start-node":
|
||||||
|
return "Start";
|
||||||
|
case "agent-node":
|
||||||
|
return `Agent #${order}`;
|
||||||
|
case "condition-node":
|
||||||
|
return `Condition #${order}`;
|
||||||
|
case "message-node":
|
||||||
|
return `Message #${order}`;
|
||||||
|
case "delay-node":
|
||||||
|
return `Delay #${order}`;
|
||||||
|
default:
|
||||||
|
return "Node";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddNode = useCallback(
|
||||||
|
(type: any, node: any) => {
|
||||||
|
const newNode: any = {
|
||||||
|
id: String(Date.now()),
|
||||||
|
type,
|
||||||
|
position: node.position,
|
||||||
|
data: {
|
||||||
|
label: getLabelFromNode(type),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setNodes((nodes) => [...nodes, newNode]);
|
||||||
|
|
||||||
|
if (node.targetId) {
|
||||||
|
const newEdge: any = {
|
||||||
|
source: node.targetId,
|
||||||
|
sourceHandle: node.handleId,
|
||||||
|
target: newNode.id,
|
||||||
|
type: "default",
|
||||||
|
};
|
||||||
|
|
||||||
|
const newsEdges: any = [...edges, newEdge];
|
||||||
|
|
||||||
|
setEdges(newsEdges);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[nodes, setNodes, edges, setEdges]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUpdateNode = useCallback(
|
||||||
|
(node: any) => {
|
||||||
|
setNodes((nodes) => {
|
||||||
|
const index = nodes.findIndex((n) => n.id === node.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
nodes[index] = node;
|
||||||
|
}
|
||||||
|
return [...nodes];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectedNode && selectedNode.id === node.id) {
|
||||||
|
setSelectedNode(node);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setNodes, selectedNode]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeleteEdge = useCallback(
|
||||||
|
(id: any) => {
|
||||||
|
setEdges((edges) => {
|
||||||
|
const left = edges.filter((edge: any) => edge.id !== id);
|
||||||
|
return left;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setEdges]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDragOver = useCallback((event: any) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.dataTransfer.dropEffect = "move";
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onDrop = useCallback(
|
||||||
|
(event: any) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = screenToFlowPosition({
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
|
});
|
||||||
|
const newNode: any = {
|
||||||
|
id: String(Date.now()),
|
||||||
|
type,
|
||||||
|
position,
|
||||||
|
data: {
|
||||||
|
label: getLabelFromNode(type),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setNodes((nodes) => [...nodes, newNode]);
|
||||||
|
},
|
||||||
|
[screenToFlowPosition, setNodes, type, getLabelFromNode]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onNodeContextMenu = useCallback(
|
||||||
|
(event: any, node: any) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (node.id === "start-node") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!localRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paneBounds = localRef.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
const x = event.clientX - paneBounds.left;
|
||||||
|
const y = event.clientY - paneBounds.top;
|
||||||
|
|
||||||
|
const menuWidth = 200;
|
||||||
|
const menuHeight = 200;
|
||||||
|
|
||||||
|
const left = x + menuWidth > paneBounds.width ? undefined : x;
|
||||||
|
const top = y + menuHeight > paneBounds.height ? undefined : y;
|
||||||
|
const right =
|
||||||
|
x + menuWidth > paneBounds.width ? paneBounds.width - x : undefined;
|
||||||
|
const bottom =
|
||||||
|
y + menuHeight > paneBounds.height ? paneBounds.height - y : undefined;
|
||||||
|
|
||||||
|
setMenu({
|
||||||
|
id: node.id,
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
right,
|
||||||
|
bottom,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setMenu]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onNodeClick = useCallback((event: any, node: any) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (node.type === "start-node") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedNode(node);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onPaneClick = useCallback(() => {
|
||||||
|
setMenu(null);
|
||||||
|
setSelectedNode(null);
|
||||||
|
setNodePanelOpen(false);
|
||||||
|
}, [setMenu, setSelectedNode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full bg-[#121212]">
|
||||||
|
<div
|
||||||
|
style={{ position: "relative", height: "100%", width: "100%" }}
|
||||||
|
ref={localRef}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
edges={edges}
|
||||||
|
edgeTypes={edgeTypes}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
onConnectStart={onConnectStart}
|
||||||
|
onConnectEnd={onConnectEnd}
|
||||||
|
connectionMode={ConnectionMode.Strict}
|
||||||
|
connectionLineType={ConnectionLineType.Bezier}
|
||||||
|
onDrop={onDrop}
|
||||||
|
onDragOver={onDragOver}
|
||||||
|
onPaneClick={onPaneClick}
|
||||||
|
onNodeClick={onNodeClick}
|
||||||
|
onNodeContextMenu={onNodeContextMenu}
|
||||||
|
colorMode="dark"
|
||||||
|
minZoom={0.1}
|
||||||
|
maxZoom={10}
|
||||||
|
fitView={false}
|
||||||
|
defaultViewport={{
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
zoom: 1,
|
||||||
|
}}
|
||||||
|
elevateEdgesOnSelect
|
||||||
|
elevateNodesOnSelect
|
||||||
|
proOptions={proOptions}
|
||||||
|
connectionLineStyle={{
|
||||||
|
stroke: "gray",
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeDashoffset: 5,
|
||||||
|
strokeDasharray: 5,
|
||||||
|
}}
|
||||||
|
defaultEdgeOptions={{
|
||||||
|
type: "default",
|
||||||
|
style: {
|
||||||
|
strokeWidth: 3,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
handleDeleteEdge,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Background color="#334155" gap={24} size={1.5} />
|
||||||
|
<MiniMap
|
||||||
|
className="bg-neutral-800/80 border border-neutral-700 rounded-lg shadow-lg"
|
||||||
|
nodeColor={(node) => {
|
||||||
|
switch (node.type) {
|
||||||
|
case "start-node":
|
||||||
|
return "#10b981";
|
||||||
|
case "agent-node":
|
||||||
|
return "#3b82f6";
|
||||||
|
case "message-node":
|
||||||
|
return "#f97316";
|
||||||
|
case "condition-node":
|
||||||
|
return "#3b82f6";
|
||||||
|
case "delay-node":
|
||||||
|
return "#eab308";
|
||||||
|
default:
|
||||||
|
return "#64748b";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
maskColor="rgba(15, 23, 42, 0.6)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controls
|
||||||
|
showInteractive={true}
|
||||||
|
showFitView={true}
|
||||||
|
orientation="vertical"
|
||||||
|
position="bottom-left"
|
||||||
|
/>
|
||||||
|
<HelperLines
|
||||||
|
horizontal={helperLineHorizontal}
|
||||||
|
vertical={helperLineVertical}
|
||||||
|
/>
|
||||||
|
{menu && <ContextMenu onClick={onPaneClick} {...menu} />}
|
||||||
|
|
||||||
|
{nodePanelOpen ? (
|
||||||
|
<Panel position="top-right">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setNodePanelOpen(false)}
|
||||||
|
className="mr-2 h-8 w-8 rounded-full bg-slate-800 border-slate-700 text-slate-400 hover:text-white hover:bg-slate-700"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<NodePanel />
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
) : (
|
||||||
|
<Panel position="top-right">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setNodePanelOpen(true)}
|
||||||
|
className="h-8 w-8 rounded-full bg-slate-800 border-slate-700 text-slate-400 hover:text-white hover:bg-slate-700"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</Panel>
|
||||||
|
)}
|
||||||
|
</ReactFlow>
|
||||||
|
|
||||||
|
{/* Overlay when form is open on smaller screens */}
|
||||||
|
{selectedNode && (
|
||||||
|
<div
|
||||||
|
className="md:hidden fixed inset-0 bg-black bg-opacity-50 z-[5] transition-opacity duration-300"
|
||||||
|
onClick={() => setSelectedNode(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="absolute left-0 top-0 z-10 h-full w-[350px] bg-neutral-900 shadow-lg transition-all duration-300 ease-in-out border-r border-neutral-700 flex flex-col"
|
||||||
|
style={{
|
||||||
|
transform: selectedNode ? "translateX(0)" : "translateX(-100%)",
|
||||||
|
opacity: selectedNode ? 1 : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedNode ? (
|
||||||
|
<NodeFormWrapper
|
||||||
|
selectedNode={selectedNode}
|
||||||
|
editingLabel={editingLabel}
|
||||||
|
setEditingLabel={setEditingLabel}
|
||||||
|
handleUpdateNode={handleUpdateNode}
|
||||||
|
setSelectedNode={setSelectedNode}
|
||||||
|
>
|
||||||
|
{selectedNode.type === "agent-node" && (
|
||||||
|
<AgentForm
|
||||||
|
selectedNode={selectedNode}
|
||||||
|
handleUpdateNode={handleUpdateNode}
|
||||||
|
setEdges={setEdges}
|
||||||
|
setIsOpen={() => {}}
|
||||||
|
setSelectedNode={setSelectedNode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedNode.type === "condition-node" && (
|
||||||
|
<ConditionForm
|
||||||
|
selectedNode={selectedNode}
|
||||||
|
handleUpdateNode={handleUpdateNode}
|
||||||
|
setEdges={setEdges}
|
||||||
|
setIsOpen={() => {}}
|
||||||
|
setSelectedNode={setSelectedNode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedNode.type === "message-node" && (
|
||||||
|
<MessageForm
|
||||||
|
selectedNode={selectedNode}
|
||||||
|
handleUpdateNode={handleUpdateNode}
|
||||||
|
setEdges={setEdges}
|
||||||
|
setIsOpen={() => {}}
|
||||||
|
setSelectedNode={setSelectedNode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedNode.type === "delay-node" && (
|
||||||
|
<DelayForm
|
||||||
|
selectedNode={selectedNode}
|
||||||
|
handleUpdateNode={handleUpdateNode}
|
||||||
|
setEdges={setEdges}
|
||||||
|
setIsOpen={() => {}}
|
||||||
|
setSelectedNode={setSelectedNode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</NodeFormWrapper>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Canva.displayName = "Canva";
|
||||||
|
|
||||||
|
export default Canva;
|
||||||
118
frontend/app/agents/workflows/ContextMenu.tsx
Normal file
118
frontend/app/agents/workflows/ContextMenu.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/ContextMenu.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
import { useReactFlow, Node, Edge } from "@xyflow/react";
|
||||||
|
import { Copy, Trash2 } from "lucide-react";
|
||||||
|
import React, { useCallback } from "react";
|
||||||
|
|
||||||
|
interface ContextMenuProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
id: string;
|
||||||
|
top?: number | string;
|
||||||
|
left?: number | string;
|
||||||
|
right?: number | string;
|
||||||
|
bottom?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ContextMenu({
|
||||||
|
id,
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
bottom,
|
||||||
|
...props
|
||||||
|
}: ContextMenuProps) {
|
||||||
|
const { getNode, setNodes, addNodes, setEdges } = useReactFlow();
|
||||||
|
|
||||||
|
const duplicateNode = useCallback(() => {
|
||||||
|
const node = getNode(id);
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
console.error(`Node with id ${id} not found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = {
|
||||||
|
x: node.position.x + 50,
|
||||||
|
y: node.position.y + 50,
|
||||||
|
};
|
||||||
|
|
||||||
|
addNodes({
|
||||||
|
...node,
|
||||||
|
id: `${node.id}-copy`,
|
||||||
|
position,
|
||||||
|
selected: false,
|
||||||
|
dragging: false,
|
||||||
|
});
|
||||||
|
}, [id, getNode, addNodes]);
|
||||||
|
|
||||||
|
const deleteNode = useCallback(() => {
|
||||||
|
setNodes((nodes: Node[]) => nodes.filter((node) => node.id !== id));
|
||||||
|
setEdges((edges: Edge[]) =>
|
||||||
|
edges.filter((edge) => edge.source !== id && edge.target !== id),
|
||||||
|
);
|
||||||
|
}, [id, setNodes, setEdges]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: top !== undefined ? `${top}px` : undefined,
|
||||||
|
left: left !== undefined ? `${left}px` : undefined,
|
||||||
|
right: right !== undefined ? `${right}px` : undefined,
|
||||||
|
bottom: bottom !== undefined ? `${bottom}px` : undefined,
|
||||||
|
zIndex: 10,
|
||||||
|
}}
|
||||||
|
className="context-menu rounded-md border p-3 shadow-lg border-neutral-700 bg-neutral-800"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<p className="mb-2 text-sm font-semibold text-neutral-200">
|
||||||
|
Actions
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={duplicateNode}
|
||||||
|
className="mb-1 flex w-full flex-row items-center rounded-md px-2 py-1 text-sm hover:bg-neutral-700"
|
||||||
|
>
|
||||||
|
<Copy
|
||||||
|
size={16}
|
||||||
|
className="mr-2 flex-shrink-0 text-blue-300"
|
||||||
|
/>
|
||||||
|
<span className="text-neutral-300">Duplicate</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={deleteNode}
|
||||||
|
className="flex w-full flex-row items-center rounded-md px-2 py-1 text-sm hover:bg-neutral-700"
|
||||||
|
>
|
||||||
|
<Trash2
|
||||||
|
size={16}
|
||||||
|
className="mr-2 flex-shrink-0 text-red-300"
|
||||||
|
/>
|
||||||
|
<span className="text-neutral-300">Delete</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
98
frontend/app/agents/workflows/HelperLines.tsx
Normal file
98
frontend/app/agents/workflows/HelperLines.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/HelperLines.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
import { ReactFlowState, useStore } from "@xyflow/react";
|
||||||
|
import { CSSProperties, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
const canvasStyle: CSSProperties = {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
position: "absolute",
|
||||||
|
zIndex: 10,
|
||||||
|
pointerEvents: "none",
|
||||||
|
};
|
||||||
|
|
||||||
|
const storeSelector = (state: ReactFlowState) => ({
|
||||||
|
width: state.width,
|
||||||
|
height: state.height,
|
||||||
|
transform: state.transform,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type HelperLinesProps = {
|
||||||
|
horizontal?: number;
|
||||||
|
vertical?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// a simple component to display the helper lines
|
||||||
|
// it puts a canvas on top of the React Flow pane and draws the lines using the canvas API
|
||||||
|
function HelperLinesRenderer({ horizontal, vertical }: HelperLinesProps) {
|
||||||
|
const { width, height, transform } = useStore(storeSelector);
|
||||||
|
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
const ctx = canvas?.getContext("2d");
|
||||||
|
|
||||||
|
if (!ctx || !canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dpi = window.devicePixelRatio;
|
||||||
|
canvas.width = width * dpi;
|
||||||
|
canvas.height = height * dpi;
|
||||||
|
|
||||||
|
ctx.scale(dpi, dpi);
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
ctx.strokeStyle = "#1d5ade";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.setLineDash([5, 5]);
|
||||||
|
|
||||||
|
if (typeof vertical === "number") {
|
||||||
|
ctx.moveTo(vertical * transform[2] + transform[0], 0);
|
||||||
|
ctx.lineTo(vertical * transform[2] + transform[0], height);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof horizontal === "number") {
|
||||||
|
ctx.moveTo(0, horizontal * transform[2] + transform[1]);
|
||||||
|
ctx.lineTo(width, horizontal * transform[2] + transform[1]);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}, [width, height, transform, horizontal, vertical]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
className="react-flow__canvas"
|
||||||
|
style={canvasStyle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HelperLinesRenderer;
|
||||||
273
frontend/app/agents/workflows/NodePanel.tsx
Normal file
273
frontend/app/agents/workflows/NodePanel.tsx
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type React from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
User,
|
||||||
|
MessageSquare,
|
||||||
|
Filter,
|
||||||
|
Clock,
|
||||||
|
Plus,
|
||||||
|
MenuSquare,
|
||||||
|
Layers,
|
||||||
|
MoveRight,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { useDnD } from "@/contexts/DnDContext";
|
||||||
|
|
||||||
|
export function NodePanel() {
|
||||||
|
const [activeTab, setActiveTab] = useState("content");
|
||||||
|
const { setType } = useDnD();
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
id: "agent-node",
|
||||||
|
name: "Agent",
|
||||||
|
icon: User,
|
||||||
|
color: "text-blue-400",
|
||||||
|
bgColor: "bg-blue-950/40",
|
||||||
|
borderColor: "border-blue-500/30",
|
||||||
|
hoverColor: "group-hover:bg-blue-900/50",
|
||||||
|
glowColor: "group-hover:shadow-blue-500/20",
|
||||||
|
description: "Add an AI agent to process messages and execute tasks",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "message-node",
|
||||||
|
name: "Message",
|
||||||
|
icon: MessageSquare,
|
||||||
|
color: "text-orange-400",
|
||||||
|
bgColor: "bg-orange-950/40",
|
||||||
|
borderColor: "border-orange-500/30",
|
||||||
|
hoverColor: "group-hover:bg-orange-900/50",
|
||||||
|
glowColor: "group-hover:shadow-orange-500/20",
|
||||||
|
description: "Send a message to users or other nodes in the workflow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
logic: [
|
||||||
|
{
|
||||||
|
id: "condition-node",
|
||||||
|
name: "Condition",
|
||||||
|
icon: Filter,
|
||||||
|
color: "text-purple-400",
|
||||||
|
bgColor: "bg-purple-950/40",
|
||||||
|
borderColor: "border-purple-500/30",
|
||||||
|
hoverColor: "group-hover:bg-purple-900/50",
|
||||||
|
glowColor: "group-hover:shadow-purple-500/20",
|
||||||
|
description:
|
||||||
|
"Create a decision point with multiple outcomes based on conditions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "delay-node",
|
||||||
|
name: "Delay",
|
||||||
|
icon: Clock,
|
||||||
|
color: "text-yellow-400",
|
||||||
|
bgColor: "bg-yellow-950/40",
|
||||||
|
borderColor: "border-yellow-500/30",
|
||||||
|
hoverColor: "group-hover:bg-yellow-900/50",
|
||||||
|
glowColor: "group-hover:shadow-yellow-500/20",
|
||||||
|
description: "Add a time delay between workflow operations",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragStart = (event: React.DragEvent, nodeType: string) => {
|
||||||
|
event.dataTransfer.setData("application/reactflow", nodeType);
|
||||||
|
event.dataTransfer.effectAllowed = "move";
|
||||||
|
setType(nodeType);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNodeAdd = (nodeType: string) => {
|
||||||
|
setType(nodeType);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-slate-900/70 backdrop-blur-md border border-slate-700/50 rounded-xl shadow-xl w-[320px] transition-all duration-300 ease-in-out overflow-hidden">
|
||||||
|
<div className="px-4 pt-4 pb-2 border-b border-slate-700/50">
|
||||||
|
<div className="flex items-center gap-2 text-slate-200">
|
||||||
|
<Layers className="h-5 w-5 text-indigo-400" />
|
||||||
|
<h3 className="font-medium">Workflow Nodes</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-400 mt-1">
|
||||||
|
Drag nodes to the canvas or click to add
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
|
<div className="px-4 pt-3">
|
||||||
|
<TabsList className="w-full bg-slate-800/50 grid grid-cols-2 p-1 rounded-lg">
|
||||||
|
<TabsTrigger
|
||||||
|
value="content"
|
||||||
|
className={cn(
|
||||||
|
"rounded-md text-xs font-medium transition-all",
|
||||||
|
"data-[state=active]:bg-gradient-to-br data-[state=active]:from-blue-900/30 data-[state=active]:to-indigo-900/30",
|
||||||
|
"data-[state=active]:text-blue-300 data-[state=active]:shadow-sm",
|
||||||
|
"data-[state=inactive]:text-slate-400"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<MenuSquare className="h-3.5 w-3.5 mr-1.5" />
|
||||||
|
Content
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="logic"
|
||||||
|
className={cn(
|
||||||
|
"rounded-md text-xs font-medium transition-all",
|
||||||
|
"data-[state=active]:bg-gradient-to-br data-[state=active]:from-yellow-900/30 data-[state=active]:to-orange-900/30",
|
||||||
|
"data-[state=active]:text-yellow-300 data-[state=active]:shadow-sm",
|
||||||
|
"data-[state=inactive]:text-slate-400"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Filter className="h-3.5 w-3.5 mr-1.5" />
|
||||||
|
Logic
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TabsContent value="content" className="p-3 space-y-2 mt-0">
|
||||||
|
{nodeTypes.content.map((node) => (
|
||||||
|
<TooltipProvider key={node.id} delayDuration={300}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div
|
||||||
|
draggable
|
||||||
|
onDragStart={(event) => onDragStart(event, node.id)}
|
||||||
|
className={cn(
|
||||||
|
"group flex items-center gap-3 p-3.5 border rounded-lg cursor-grab transition-all duration-300",
|
||||||
|
"backdrop-blur-sm hover:shadow-lg",
|
||||||
|
node.borderColor,
|
||||||
|
node.bgColor,
|
||||||
|
node.glowColor
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center w-9 h-9 rounded-lg transition-all duration-300",
|
||||||
|
"bg-slate-800/80 group-hover:scale-105",
|
||||||
|
node.hoverColor
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<node.icon className={cn("h-5 w-5", node.color)} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<span
|
||||||
|
className={cn("font-medium block text-sm", node.color)}
|
||||||
|
>
|
||||||
|
{node.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-slate-400 truncate block">
|
||||||
|
{node.description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={() => handleNodeAdd(node.id)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center h-7 w-7 rounded-md bg-slate-800/60 text-slate-400",
|
||||||
|
"hover:bg-gradient-to-r hover:text-white transition-all",
|
||||||
|
node.id === "agent-node"
|
||||||
|
? "hover:from-blue-800 hover:to-blue-600"
|
||||||
|
: "hover:from-orange-800 hover:to-orange-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Plus size={16} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="right"
|
||||||
|
className="bg-slate-900 border-slate-700 text-slate-200"
|
||||||
|
>
|
||||||
|
<div className="p-1 max-w-[200px]">
|
||||||
|
<p className="font-medium text-sm">{node.name} Node</p>
|
||||||
|
<p className="text-xs text-slate-400 mt-1">
|
||||||
|
{node.description}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center mt-2 pt-2 border-t border-slate-700/50 text-xs text-slate-400">
|
||||||
|
<MoveRight className="h-3 w-3 mr-1.5" />
|
||||||
|
<span>Drag to canvas or click + to add</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
))}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="logic" className="p-3 space-y-2 mt-0">
|
||||||
|
{nodeTypes.logic.map((node) => (
|
||||||
|
<TooltipProvider key={node.id} delayDuration={300}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div
|
||||||
|
draggable
|
||||||
|
onDragStart={(event) => onDragStart(event, node.id)}
|
||||||
|
className={cn(
|
||||||
|
"group flex items-center gap-3 p-3.5 border rounded-lg cursor-grab transition-all duration-300",
|
||||||
|
"backdrop-blur-sm hover:shadow-lg",
|
||||||
|
node.borderColor,
|
||||||
|
node.bgColor,
|
||||||
|
node.glowColor
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center w-9 h-9 rounded-lg transition-all duration-300",
|
||||||
|
"bg-slate-800/80 group-hover:scale-105",
|
||||||
|
node.hoverColor
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<node.icon className={cn("h-5 w-5", node.color)} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<span
|
||||||
|
className={cn("font-medium block text-sm", node.color)}
|
||||||
|
>
|
||||||
|
{node.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-slate-400 truncate block">
|
||||||
|
{node.description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={() => handleNodeAdd(node.id)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center h-7 w-7 rounded-md bg-slate-800/60 text-slate-400",
|
||||||
|
"hover:bg-gradient-to-r hover:text-white transition-all",
|
||||||
|
node.id === "condition-node"
|
||||||
|
? "hover:from-purple-800 hover:to-purple-600"
|
||||||
|
: "hover:from-yellow-800 hover:to-yellow-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Plus size={16} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="right"
|
||||||
|
className="bg-slate-900 border-slate-700 text-slate-200"
|
||||||
|
>
|
||||||
|
<div className="p-1 max-w-[200px]">
|
||||||
|
<p className="font-medium text-sm">{node.name} Node</p>
|
||||||
|
<p className="text-xs text-slate-400 mt-1">
|
||||||
|
{node.description}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center mt-2 pt-2 border-t border-slate-700/50 text-xs text-slate-400">
|
||||||
|
<MoveRight className="h-3 w-3 mr-1.5" />
|
||||||
|
<span>Drag to canvas or click + to add</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
))}
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
188
frontend/app/agents/workflows/canva.css
Normal file
188
frontend/app/agents/workflows/canva.css
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/canva.css │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
.react-flow.dark {
|
||||||
|
--xy-background-color-default: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__panel {
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__controls {
|
||||||
|
background-color: rgba(30, 30, 30, 0.8);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(70, 70, 70, 0.5);
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__controls-button {
|
||||||
|
padding: 6px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin: 4px 0;
|
||||||
|
border: none !important;
|
||||||
|
background-color: rgba(40, 40, 40, 0.8) !important;
|
||||||
|
color: #a0a0a0 !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__controls-button:hover {
|
||||||
|
background-color: rgba(60, 60, 60, 0.9) !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__controls-button svg {
|
||||||
|
fill: currentColor;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__controls-button[data-action="zoom-in"] {
|
||||||
|
border-bottom: 1px solid rgba(70, 70, 70, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__controls-button[data-action="zoom-out"] {
|
||||||
|
border-bottom: 1px solid rgba(70, 70, 70, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar for all browsers while maintaining scroll functionality */
|
||||||
|
.scrollbar-hide {
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome, Safari and Opera */
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animated dashed edge for flow edges */
|
||||||
|
.edge-dashed-animated {
|
||||||
|
stroke-dasharray: 6;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
animation: dash 0.5s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dash {
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: -12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edges styling */
|
||||||
|
.react-flow__edge-path {
|
||||||
|
stroke: #10b981; /* Emerald color for edges */
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__edge.selected .react-flow__edge-path {
|
||||||
|
stroke: #3b82f6; /* Blue color for selected edges */
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Node handle styling */
|
||||||
|
.react-flow__handle {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #10b981;
|
||||||
|
border: 2px solid #0d9488;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__handle:hover {
|
||||||
|
background-color: #3b82f6;
|
||||||
|
border-color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nó em execução */
|
||||||
|
.active-execution-node {
|
||||||
|
animation: pulse-execution 1.5s infinite;
|
||||||
|
filter: drop-shadow(0 0 15px rgba(5, 212, 114, 0.9));
|
||||||
|
z-index: 10;
|
||||||
|
transform: scale(1.03);
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-execution {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(5, 212, 114, 0.9);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 15px rgba(5, 212, 114, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(5, 212, 114, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__node[data-is-executing="true"] {
|
||||||
|
filter: drop-shadow(0 0 15px rgba(5, 212, 114, 0.9));
|
||||||
|
transform: scale(1.03);
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__node[data-is-executing="true"]::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
left: -5px;
|
||||||
|
right: -5px;
|
||||||
|
bottom: -5px;
|
||||||
|
border: 2px solid #05d472;
|
||||||
|
border-radius: 8px;
|
||||||
|
animation: pulse-border 1.5s infinite;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-border {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
119
frontend/app/agents/workflows/edges/DefaultEdge.tsx
Normal file
119
frontend/app/agents/workflows/edges/DefaultEdge.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/edges/DefaultEdge.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
BaseEdge,
|
||||||
|
EdgeLabelRenderer,
|
||||||
|
EdgeProps,
|
||||||
|
getSmoothStepPath,
|
||||||
|
useReactFlow,
|
||||||
|
} from "@xyflow/react";
|
||||||
|
import { Trash2 } from "lucide-react";
|
||||||
|
|
||||||
|
export default function DefaultEdge({
|
||||||
|
id,
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
sourcePosition,
|
||||||
|
targetPosition,
|
||||||
|
style = {},
|
||||||
|
selected,
|
||||||
|
}: EdgeProps) {
|
||||||
|
const { setEdges } = useReactFlow();
|
||||||
|
|
||||||
|
const [edgePath, labelX, labelY] = getSmoothStepPath({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
sourcePosition,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
targetPosition,
|
||||||
|
borderRadius: 15,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onEdgeClick = () => {
|
||||||
|
console.log("onEdgeClick", id);
|
||||||
|
setEdges((edges) => edges.filter((edge) => edge.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<svg>
|
||||||
|
<defs>
|
||||||
|
<marker
|
||||||
|
id="arrowhead"
|
||||||
|
viewBox="0 0 10 16"
|
||||||
|
refX="12"
|
||||||
|
refY="8"
|
||||||
|
markerWidth="4"
|
||||||
|
markerHeight="5"
|
||||||
|
orient="auto"
|
||||||
|
>
|
||||||
|
<path d="M 0 0 L 10 8 L 0 16 z" fill="gray" />
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<BaseEdge
|
||||||
|
id={id}
|
||||||
|
path={edgePath}
|
||||||
|
className="edge-dashed-animated"
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
stroke: '#10B981',
|
||||||
|
strokeWidth: 3,
|
||||||
|
}}
|
||||||
|
markerEnd="url(#arrowhead)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{selected && (
|
||||||
|
<EdgeLabelRenderer>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||||
|
fontSize: 12,
|
||||||
|
pointerEvents: "all",
|
||||||
|
zIndex: 1000,
|
||||||
|
}}
|
||||||
|
className="nodrag nopan"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="rounded-full bg-white p-1 shadow-md"
|
||||||
|
onClick={onEdgeClick}
|
||||||
|
>
|
||||||
|
<Trash2 className="text-red-500" size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
frontend/app/agents/workflows/edges/index.ts
Normal file
41
frontend/app/agents/workflows/edges/index.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/edges/index.ts │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
import type { Edge, EdgeTypes } from "@xyflow/react";
|
||||||
|
|
||||||
|
import DefaultEdge from "./DefaultEdge";
|
||||||
|
|
||||||
|
export const initialEdges = [
|
||||||
|
// { id: "a->c", source: "a", target: "c", animated: true },
|
||||||
|
// { id: "b->d", source: "b", target: "d" },
|
||||||
|
// { id: "c->d", source: "c", target: "d", animated: true },
|
||||||
|
] satisfies Edge[];
|
||||||
|
|
||||||
|
export const edgeTypes = {
|
||||||
|
default: DefaultEdge,
|
||||||
|
} satisfies EdgeTypes;
|
||||||
166
frontend/app/agents/workflows/nodes/BaseNode.tsx
Normal file
166
frontend/app/agents/workflows/nodes/BaseNode.tsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/nodes/BaseNode.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
import { Handle, Position } from "@xyflow/react";
|
||||||
|
import React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useDnD } from "@/contexts/DnDContext";
|
||||||
|
|
||||||
|
export function BaseNode({
|
||||||
|
selected,
|
||||||
|
hasTarget,
|
||||||
|
children,
|
||||||
|
borderColor,
|
||||||
|
isExecuting
|
||||||
|
}: {
|
||||||
|
selected: boolean;
|
||||||
|
hasTarget: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
borderColor: string;
|
||||||
|
isExecuting?: boolean;
|
||||||
|
}) {
|
||||||
|
const { pointerEvents } = useDnD();
|
||||||
|
|
||||||
|
// Border and background color mapping
|
||||||
|
const colorStyles = {
|
||||||
|
blue: {
|
||||||
|
border: "border-blue-700/70 hover:border-blue-500",
|
||||||
|
gradient: "bg-gradient-to-br from-blue-950/40 to-neutral-900/90",
|
||||||
|
glow: "shadow-[0_0_15px_rgba(59,130,246,0.15)]",
|
||||||
|
selectedGlow: "shadow-[0_0_25px_rgba(59,130,246,0.3)]"
|
||||||
|
},
|
||||||
|
orange: {
|
||||||
|
border: "border-orange-700/70 hover:border-orange-500",
|
||||||
|
gradient: "bg-gradient-to-br from-orange-950/40 to-neutral-900/90",
|
||||||
|
glow: "shadow-[0_0_15px_rgba(249,115,22,0.15)]",
|
||||||
|
selectedGlow: "shadow-[0_0_25px_rgba(249,115,22,0.3)]"
|
||||||
|
},
|
||||||
|
green: {
|
||||||
|
border: "border-green-700/70 hover:border-green-500",
|
||||||
|
gradient: "bg-gradient-to-br from-green-950/40 to-neutral-900/90",
|
||||||
|
glow: "shadow-[0_0_15px_rgba(34,197,94,0.15)]",
|
||||||
|
selectedGlow: "shadow-[0_0_25px_rgba(34,197,94,0.3)]"
|
||||||
|
},
|
||||||
|
red: {
|
||||||
|
border: "border-red-700/70 hover:border-red-500",
|
||||||
|
gradient: "bg-gradient-to-br from-red-950/40 to-neutral-900/90",
|
||||||
|
glow: "shadow-[0_0_15px_rgba(239,68,68,0.15)]",
|
||||||
|
selectedGlow: "shadow-[0_0_25px_rgba(239,68,68,0.3)]"
|
||||||
|
},
|
||||||
|
yellow: {
|
||||||
|
border: "border-yellow-700/70 hover:border-yellow-500",
|
||||||
|
gradient: "bg-gradient-to-br from-yellow-950/40 to-neutral-900/90",
|
||||||
|
glow: "shadow-[0_0_15px_rgba(234,179,8,0.15)]",
|
||||||
|
selectedGlow: "shadow-[0_0_25px_rgba(234,179,8,0.3)]"
|
||||||
|
},
|
||||||
|
purple: {
|
||||||
|
border: "border-purple-700/70 hover:border-purple-500",
|
||||||
|
gradient: "bg-gradient-to-br from-purple-950/40 to-neutral-900/90",
|
||||||
|
glow: "shadow-[0_0_15px_rgba(168,85,247,0.15)]",
|
||||||
|
selectedGlow: "shadow-[0_0_25px_rgba(168,85,247,0.3)]"
|
||||||
|
},
|
||||||
|
indigo: {
|
||||||
|
border: "border-indigo-700/70 hover:border-indigo-500",
|
||||||
|
gradient: "bg-gradient-to-br from-indigo-950/40 to-neutral-900/90",
|
||||||
|
glow: "shadow-[0_0_15px_rgba(99,102,241,0.15)]",
|
||||||
|
selectedGlow: "shadow-[0_0_25px_rgba(99,102,241,0.3)]"
|
||||||
|
},
|
||||||
|
pink: {
|
||||||
|
border: "border-pink-700/70 hover:border-pink-500",
|
||||||
|
gradient: "bg-gradient-to-br from-pink-950/40 to-neutral-900/90",
|
||||||
|
glow: "shadow-[0_0_15px_rgba(236,72,153,0.15)]",
|
||||||
|
selectedGlow: "shadow-[0_0_25px_rgba(236,72,153,0.3)]"
|
||||||
|
},
|
||||||
|
emerald: {
|
||||||
|
border: "border-emerald-700/70 hover:border-emerald-500",
|
||||||
|
gradient: "bg-gradient-to-br from-emerald-950/40 to-neutral-900/90",
|
||||||
|
glow: "shadow-[0_0_15px_rgba(16,185,129,0.15)]",
|
||||||
|
selectedGlow: "shadow-[0_0_25px_rgba(16,185,129,0.3)]"
|
||||||
|
},
|
||||||
|
slate: {
|
||||||
|
border: "border-slate-700/70 hover:border-slate-500",
|
||||||
|
gradient: "bg-gradient-to-br from-slate-800/40 to-neutral-900/90",
|
||||||
|
glow: "shadow-[0_0_15px_rgba(100,116,139,0.15)]",
|
||||||
|
selectedGlow: "shadow-[0_0_25px_rgba(100,116,139,0.3)]"
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default to blue if color not in mapping
|
||||||
|
const colorStyle = colorStyles[borderColor as keyof typeof colorStyles] || colorStyles.blue;
|
||||||
|
|
||||||
|
// Selected styles
|
||||||
|
const selectedStyle = {
|
||||||
|
border: "border-green-500/90",
|
||||||
|
glow: colorStyle.selectedGlow
|
||||||
|
};
|
||||||
|
|
||||||
|
// Executing styles
|
||||||
|
const executingStyle = {
|
||||||
|
border: "border-emerald-500",
|
||||||
|
glow: "shadow-[0_0_25px_rgba(5,212,114,0.5)]"
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative z-0 w-[350px] rounded-2xl p-4 border-2 backdrop-blur-sm transition-all duration-300",
|
||||||
|
"shadow-lg hover:shadow-xl",
|
||||||
|
isExecuting ? executingStyle.glow : selected ? selectedStyle.glow : colorStyle.glow,
|
||||||
|
isExecuting ? executingStyle.border : selected ? selectedStyle.border : colorStyle.border,
|
||||||
|
colorStyle.gradient,
|
||||||
|
isExecuting && "active-execution-node"
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
backdropFilter: "blur(12px)",
|
||||||
|
}}
|
||||||
|
data-is-executing={isExecuting ? "true" : "false"}
|
||||||
|
>
|
||||||
|
{hasTarget && (
|
||||||
|
<Handle
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: "50%",
|
||||||
|
top: "50%",
|
||||||
|
width: "100%",
|
||||||
|
borderRadius: "15px",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
border: "none",
|
||||||
|
pointerEvents: pointerEvents === "none" ? "none" : "auto",
|
||||||
|
}}
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/nodes/components/agent/AgentChatMessageList.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 14, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
|
import { ChevronDown, ChevronRight, Copy, Check } from "lucide-react";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Agent } from "@/types/agent";
|
||||||
|
import { InlineDataAttachments } from "@/app/chat/components/InlineDataAttachments";
|
||||||
|
|
||||||
|
interface FunctionMessageContent {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
author?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatMessage {
|
||||||
|
id: string;
|
||||||
|
content: {
|
||||||
|
parts: any[];
|
||||||
|
role: string;
|
||||||
|
};
|
||||||
|
author: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AgentChatMessageListProps {
|
||||||
|
messages: ChatMessage[];
|
||||||
|
agent: Agent;
|
||||||
|
expandedFunctions: Record<string, boolean>;
|
||||||
|
toggleFunctionExpansion: (messageId: string) => void;
|
||||||
|
getMessageText: (message: ChatMessage) => string | FunctionMessageContent;
|
||||||
|
containsMarkdown: (text: string) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgentChatMessageList({
|
||||||
|
messages,
|
||||||
|
agent,
|
||||||
|
expandedFunctions,
|
||||||
|
toggleFunctionExpansion,
|
||||||
|
getMessageText,
|
||||||
|
containsMarkdown,
|
||||||
|
}: AgentChatMessageListProps) {
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Scroll to bottom whenever messages change
|
||||||
|
useEffect(() => {
|
||||||
|
if (messagesEndRef.current) {
|
||||||
|
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{messages.map((message) => {
|
||||||
|
const messageContent = getMessageText(message);
|
||||||
|
const isExpanded = expandedFunctions[message.id] || false;
|
||||||
|
const isUser = message.author === "user";
|
||||||
|
const agentColor = "bg-emerald-400";
|
||||||
|
const hasFunctionCall = message.content.parts.some(
|
||||||
|
(part) => part.functionCall || part.function_call
|
||||||
|
);
|
||||||
|
const hasFunctionResponse = message.content.parts.some(
|
||||||
|
(part) => part.functionResponse || part.function_response
|
||||||
|
);
|
||||||
|
const isFunctionMessage = hasFunctionCall || hasFunctionResponse;
|
||||||
|
const isTaskExecutor = typeof messageContent === "object" &&
|
||||||
|
"author" in messageContent &&
|
||||||
|
typeof messageContent.author === "string" &&
|
||||||
|
messageContent.author.endsWith("- Task executor");
|
||||||
|
|
||||||
|
const inlineDataParts = message.content.parts.filter(part => part.inline_data);
|
||||||
|
const hasInlineData = inlineDataParts.length > 0;
|
||||||
|
|
||||||
|
const isWorkflowNode = message.author && message.author.startsWith('workflow-node:');
|
||||||
|
const nodeId = isWorkflowNode ? message.author.split(':')[1] : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={message.id}
|
||||||
|
className="flex w-full"
|
||||||
|
style={{
|
||||||
|
justifyContent: isUser ? "flex-end" : "flex-start"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex gap-3 max-w-[90%]"
|
||||||
|
style={{
|
||||||
|
flexDirection: isUser ? "row-reverse" : "row"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar className={isUser ? "bg-[#333]" : agentColor}>
|
||||||
|
<AvatarFallback>
|
||||||
|
{isUser ? "U" : agent.name[0] || "A"}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div
|
||||||
|
className={`rounded-lg p-3 ${isFunctionMessage || isTaskExecutor
|
||||||
|
? "bg-[#333] text-emerald-400 font-mono text-sm"
|
||||||
|
: isUser
|
||||||
|
? "bg-emerald-400 text-black"
|
||||||
|
: "bg-[#222] text-white"
|
||||||
|
} overflow-hidden relative group`}
|
||||||
|
style={{
|
||||||
|
wordBreak: "break-word",
|
||||||
|
maxWidth: "calc(100% - 3rem)",
|
||||||
|
width: "100%",
|
||||||
|
...(isWorkflowNode ? {
|
||||||
|
borderLeft: '3px solid #05d472',
|
||||||
|
boxShadow: '0 0 10px rgba(5, 212, 114, 0.2)'
|
||||||
|
} : {})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isWorkflowNode && (
|
||||||
|
<div className="text-xs text-emerald-500 mb-1 flex items-center space-x-1 bg-emerald-950/30 p-1 rounded">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-3 w-3 mr-1">
|
||||||
|
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
||||||
|
</svg>
|
||||||
|
<span>Node {nodeId} is running</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isFunctionMessage || isTaskExecutor ? (
|
||||||
|
<div className="w-full">
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 cursor-pointer hover:bg-[#444] rounded px-1 py-0.5 transition-colors"
|
||||||
|
onClick={() => toggleFunctionExpansion(message.id)}
|
||||||
|
>
|
||||||
|
{typeof messageContent === "object" &&
|
||||||
|
"title" in messageContent && (
|
||||||
|
<>
|
||||||
|
<div className="flex-1 font-semibold">
|
||||||
|
{(messageContent as FunctionMessageContent).title}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center w-5 h-5 text-emerald-400">
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isTaskExecutor && (
|
||||||
|
<>
|
||||||
|
<div className="flex-1 font-semibold">
|
||||||
|
Task Execution
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center w-5 h-5 text-emerald-400">
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="mt-2 pt-2 border-t border-[#555]">
|
||||||
|
{typeof messageContent === "object" &&
|
||||||
|
"content" in messageContent && (
|
||||||
|
<div className="max-w-full overflow-x-auto">
|
||||||
|
<pre className="whitespace-pre-wrap text-xs max-w-full" style={{
|
||||||
|
wordWrap: "break-word",
|
||||||
|
maxWidth: "100%",
|
||||||
|
wordBreak: "break-all"
|
||||||
|
}}>
|
||||||
|
{(messageContent as FunctionMessageContent).content}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="markdown-content break-words max-w-full overflow-x-auto">
|
||||||
|
{typeof messageContent === "object" &&
|
||||||
|
"author" in messageContent &&
|
||||||
|
messageContent.author !== "user" &&
|
||||||
|
!isTaskExecutor && (
|
||||||
|
<div className="text-xs text-neutral-400 mb-1">
|
||||||
|
{messageContent.author}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{((typeof messageContent === "string" &&
|
||||||
|
containsMarkdown(messageContent)) ||
|
||||||
|
(typeof messageContent === "object" &&
|
||||||
|
"content" in messageContent &&
|
||||||
|
typeof messageContent.content === "string" &&
|
||||||
|
containsMarkdown(messageContent.content))) &&
|
||||||
|
!isTaskExecutor ? (
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
components={{
|
||||||
|
h1: ({ ...props }) => (
|
||||||
|
<h1 className="text-xl font-bold my-4" {...props} />
|
||||||
|
),
|
||||||
|
h2: ({ ...props }) => (
|
||||||
|
<h2 className="text-lg font-bold my-3" {...props} />
|
||||||
|
),
|
||||||
|
h3: ({ ...props }) => (
|
||||||
|
<h3 className="text-base font-bold my-2" {...props} />
|
||||||
|
),
|
||||||
|
h4: ({ ...props }) => (
|
||||||
|
<h4 className="font-semibold my-2" {...props} />
|
||||||
|
),
|
||||||
|
p: ({ ...props }) => <p className="mb-3" {...props} />,
|
||||||
|
ul: ({ ...props }) => (
|
||||||
|
<ul
|
||||||
|
className="list-disc pl-6 mb-3 space-y-1"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
ol: ({ ...props }) => (
|
||||||
|
<ol
|
||||||
|
className="list-decimal pl-6 mb-3 space-y-1"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
li: ({ ...props }) => <li className="mb-1" {...props} />,
|
||||||
|
a: ({ ...props }) => (
|
||||||
|
<a
|
||||||
|
className="text-emerald-400 underline hover:opacity-80 transition-opacity"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
blockquote: ({ ...props }) => (
|
||||||
|
<blockquote
|
||||||
|
className="border-l-4 border-[#444] pl-4 py-1 italic my-3 text-neutral-300"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
code: ({ className, children, ...props }: any) => {
|
||||||
|
const match = /language-(\w+)/.exec(className || "");
|
||||||
|
const isInline = !match && typeof children === "string" && !children.includes("\n");
|
||||||
|
|
||||||
|
if (isInline) {
|
||||||
|
return (
|
||||||
|
<code
|
||||||
|
className="bg-[#333] px-1.5 py-0.5 rounded text-emerald-400 text-sm font-mono"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="my-3 relative">
|
||||||
|
<div className="bg-[#1a1a1a] rounded-t-md border-b border-[#333] p-2 text-xs text-neutral-400">
|
||||||
|
<span>{match?.[1] || "Code"}</span>
|
||||||
|
</div>
|
||||||
|
<pre className="bg-[#1a1a1a] p-3 rounded-b-md overflow-x-auto whitespace-pre text-sm">
|
||||||
|
<code {...props}>{children}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
table: ({ ...props }) => (
|
||||||
|
<div className="overflow-x-auto my-3">
|
||||||
|
<table
|
||||||
|
className="min-w-full border border-[#333] rounded"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
thead: ({ ...props }) => (
|
||||||
|
<thead className="bg-[#1a1a1a]" {...props} />
|
||||||
|
),
|
||||||
|
tbody: ({ ...props }) => <tbody {...props} />,
|
||||||
|
tr: ({ ...props }) => (
|
||||||
|
<tr
|
||||||
|
className="border-b border-[#333] last:border-0"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
th: ({ ...props }) => (
|
||||||
|
<th
|
||||||
|
className="px-4 py-2 text-left text-xs font-semibold text-neutral-300"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
td: ({ ...props }) => (
|
||||||
|
<td className="px-4 py-2 text-sm" {...props} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{typeof messageContent === "string"
|
||||||
|
? messageContent
|
||||||
|
: messageContent.content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
) : (
|
||||||
|
<div className="whitespace-pre-wrap">
|
||||||
|
{typeof messageContent === "string"
|
||||||
|
? messageContent
|
||||||
|
: messageContent.content}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasInlineData && (
|
||||||
|
<InlineDataAttachments parts={inlineDataParts} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{/* Empty div at the end for auto-scrolling */}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,631 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/nodes/components/agent/AgentForm.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
/* eslint-disable jsx-a11y/alt-text */
|
||||||
|
import { useEdges, useNodes } from "@xyflow/react";
|
||||||
|
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
|
||||||
|
import { Agent } from "@/types/agent";
|
||||||
|
import { listAgents, listFolders, Folder, getAgent } from "@/services/agentService";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { User, Loader2, Search, FolderIcon, Trash2, Play, MessageSquare, PlayIcon, Plus, X } from "lucide-react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { AgentForm as GlobalAgentForm } from "@/app/agents/forms/AgentForm";
|
||||||
|
import { ApiKey, listApiKeys } from "@/services/agentService";
|
||||||
|
import { listMCPServers } from "@/services/mcpServerService";
|
||||||
|
import { availableModels } from "@/types/aiModels";
|
||||||
|
import { MCPServer } from "@/types/mcpServer";
|
||||||
|
import { AgentTestChatModal } from "./AgentTestChatModal";
|
||||||
|
import { sanitizeAgentName, escapePromptBraces } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
const user = typeof window !== "undefined" ? JSON.parse(localStorage.getItem("user") || '{}') : {};
|
||||||
|
const clientId: string = user?.client_id ? String(user.client_id) : "";
|
||||||
|
|
||||||
|
const agentListStyles = {
|
||||||
|
scrollbarWidth: 'none', /* Firefox */
|
||||||
|
msOverflowStyle: 'none', /* IE and Edge */
|
||||||
|
'::-webkit-scrollbar': {
|
||||||
|
display: 'none' /* Chrome, Safari and Opera */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AgentForm({ selectedNode, handleUpdateNode, setEdges, setIsOpen, setSelectedNode }: {
|
||||||
|
selectedNode: any;
|
||||||
|
handleUpdateNode: any;
|
||||||
|
setEdges: any;
|
||||||
|
setIsOpen: any;
|
||||||
|
setSelectedNode: any;
|
||||||
|
}) {
|
||||||
|
const [node, setNode] = useState(selectedNode);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [loadingFolders, setLoadingFolders] = useState(true);
|
||||||
|
const [loadingCurrentAgent, setLoadingCurrentAgent] = useState(false);
|
||||||
|
const [agents, setAgents] = useState<Agent[]>([]);
|
||||||
|
const [allAgents, setAllAgents] = useState<Agent[]>([]);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [folders, setFolders] = useState<Folder[]>([]);
|
||||||
|
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null);
|
||||||
|
const [selectedAgentType, setSelectedAgentType] = useState<string | null>(null);
|
||||||
|
const [agentTypes, setAgentTypes] = useState<string[]>([]);
|
||||||
|
const [agentFolderId, setAgentFolderId] = useState<string | null>(null);
|
||||||
|
const edges = useEdges();
|
||||||
|
const nodes = useNodes();
|
||||||
|
const [isAgentDialogOpen, setIsAgentDialogOpen] = useState(false);
|
||||||
|
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
|
||||||
|
const [availableMCPs, setAvailableMCPs] = useState<MCPServer[]>([]);
|
||||||
|
const [newAgent, setNewAgent] = useState<Partial<Agent>>({
|
||||||
|
client_id: clientId || "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
type: "llm",
|
||||||
|
model: "openai/gpt-4.1-nano",
|
||||||
|
instruction: "",
|
||||||
|
api_key_id: "",
|
||||||
|
config: {
|
||||||
|
tools: [],
|
||||||
|
mcp_servers: [],
|
||||||
|
custom_mcp_servers: [],
|
||||||
|
custom_tools: { http_tools: [] },
|
||||||
|
sub_agents: [],
|
||||||
|
agent_tools: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isTestModalOpen, setIsTestModalOpen] = useState(false);
|
||||||
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
|
|
||||||
|
// Access the canvas reference from localStorage
|
||||||
|
const canvasRef = useRef<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// When the component is mounted, check if there is a canvas reference in the global context
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const workflowsPage = document.querySelector('[data-workflow-page="true"]');
|
||||||
|
if (workflowsPage) {
|
||||||
|
// If we are on the workflows page, try to access the canvas ref
|
||||||
|
const canvasElement = workflowsPage.querySelector('[data-canvas-ref="true"]');
|
||||||
|
if (canvasElement && (canvasElement as any).__reactRef) {
|
||||||
|
canvasRef.current = (canvasElement as any).__reactRef.current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const connectedNode = useMemo(() => {
|
||||||
|
const edge = edges.find((edge: any) => edge.source === selectedNode.id);
|
||||||
|
if (!edge) return null;
|
||||||
|
const node = nodes.find((node: any) => node.id === edge.target);
|
||||||
|
return node || null;
|
||||||
|
}, [edges, nodes, selectedNode.id]);
|
||||||
|
|
||||||
|
const currentAgent = typeof window !== "undefined" ?
|
||||||
|
JSON.parse(localStorage.getItem("current_workflow_agent") || '{}') : {};
|
||||||
|
const currentAgentId = currentAgent?.id;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedNode) {
|
||||||
|
setNode(selectedNode);
|
||||||
|
}
|
||||||
|
}, [selectedNode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!clientId) return;
|
||||||
|
setLoadingFolders(true);
|
||||||
|
listFolders(clientId)
|
||||||
|
.then((res) => {
|
||||||
|
setFolders(res.data);
|
||||||
|
})
|
||||||
|
.catch((error) => console.error("Error loading folders:", error))
|
||||||
|
.finally(() => setLoadingFolders(false));
|
||||||
|
}, [clientId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentAgentId || !clientId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingCurrentAgent(true);
|
||||||
|
|
||||||
|
getAgent(currentAgentId, clientId)
|
||||||
|
.then((res) => {
|
||||||
|
const agent = res.data;
|
||||||
|
if (agent.folder_id) {
|
||||||
|
setAgentFolderId(agent.folder_id);
|
||||||
|
setSelectedFolderId(agent.folder_id);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => console.error("Error loading current agent:", error))
|
||||||
|
.finally(() => setLoadingCurrentAgent(false));
|
||||||
|
}, [currentAgentId, clientId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!clientId) return;
|
||||||
|
|
||||||
|
if (loadingFolders || loadingCurrentAgent) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
listAgents(clientId, 0, 100, selectedFolderId || undefined)
|
||||||
|
.then((res) => {
|
||||||
|
const filteredAgents = res.data.filter((agent: Agent) => agent.id !== currentAgentId);
|
||||||
|
setAllAgents(filteredAgents);
|
||||||
|
setAgents(filteredAgents);
|
||||||
|
|
||||||
|
// Extract unique agent types
|
||||||
|
const types = [...new Set(filteredAgents.map(agent => agent.type))].filter(Boolean);
|
||||||
|
setAgentTypes(types);
|
||||||
|
})
|
||||||
|
.catch((error) => console.error("Error loading agents:", error))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [clientId, currentAgentId, selectedFolderId, loadingFolders, loadingCurrentAgent]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Apply all filters: search, folder, and type
|
||||||
|
let filtered = allAgents;
|
||||||
|
|
||||||
|
// Search filter is applied in a separate effect
|
||||||
|
if (searchQuery.trim() !== "") {
|
||||||
|
filtered = filtered.filter((agent) =>
|
||||||
|
agent.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
agent.description?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply agent type filter
|
||||||
|
if (selectedAgentType) {
|
||||||
|
filtered = filtered.filter(agent => agent.type === selectedAgentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAgents(filtered);
|
||||||
|
}, [searchQuery, selectedAgentType, allAgents]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!clientId) return;
|
||||||
|
listApiKeys(clientId).then((res) => setApiKeys(res.data));
|
||||||
|
listMCPServers().then((res) => setAvailableMCPs(res.data));
|
||||||
|
}, [clientId]);
|
||||||
|
|
||||||
|
const handleDeleteEdge = useCallback(() => {
|
||||||
|
const id = edges.find((edge: any) => edge.source === selectedNode.id)?.id;
|
||||||
|
setEdges((edges: any) => {
|
||||||
|
const left = edges.filter((edge: any) => edge.id !== id);
|
||||||
|
return left;
|
||||||
|
});
|
||||||
|
}, [nodes, edges, selectedNode, setEdges]);
|
||||||
|
|
||||||
|
const handleSelectAgent = (agent: Agent) => {
|
||||||
|
const updatedNode = {
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
agent,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setNode(updatedNode);
|
||||||
|
handleUpdateNode(updatedNode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAgentTypeName = (type: string) => {
|
||||||
|
const agentTypes: Record<string, string> = {
|
||||||
|
llm: "LLM Agent",
|
||||||
|
a2a: "A2A Agent",
|
||||||
|
sequential: "Sequential Agent",
|
||||||
|
parallel: "Parallel Agent",
|
||||||
|
loop: "Loop Agent",
|
||||||
|
workflow: "Workflow Agent",
|
||||||
|
task: "Task Agent",
|
||||||
|
};
|
||||||
|
return agentTypes[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenAgentDialog = () => {
|
||||||
|
setNewAgent({
|
||||||
|
client_id: clientId || "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
type: "llm",
|
||||||
|
model: "openai/gpt-4.1-nano",
|
||||||
|
instruction: "",
|
||||||
|
api_key_id: "",
|
||||||
|
config: {
|
||||||
|
tools: [],
|
||||||
|
mcp_servers: [],
|
||||||
|
custom_mcp_servers: [],
|
||||||
|
custom_tools: { http_tools: [] },
|
||||||
|
sub_agents: [],
|
||||||
|
agent_tools: [],
|
||||||
|
},
|
||||||
|
folder_id: selectedFolderId || undefined,
|
||||||
|
});
|
||||||
|
setIsAgentDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveAgent = async (agentData: Partial<Agent>) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const sanitizedData = {
|
||||||
|
...agentData,
|
||||||
|
client_id: clientId,
|
||||||
|
name: agentData.name ? sanitizeAgentName(agentData.name) : agentData.name,
|
||||||
|
instruction: agentData.instruction ? escapePromptBraces(agentData.instruction) : agentData.instruction
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEditMode && node.data.agent?.id) {
|
||||||
|
// Update existing agent
|
||||||
|
const { updateAgent } = await import("@/services/agentService");
|
||||||
|
const updated = await updateAgent(node.data.agent.id, sanitizedData as any);
|
||||||
|
|
||||||
|
// Refresh the agent list
|
||||||
|
const res = await listAgents(clientId, 0, 100, selectedFolderId || undefined);
|
||||||
|
const filteredAgents = res.data.filter((agent: Agent) => agent.id !== currentAgentId);
|
||||||
|
setAllAgents(filteredAgents);
|
||||||
|
setAgents(filteredAgents);
|
||||||
|
|
||||||
|
if (updated.data) {
|
||||||
|
handleSelectAgent(updated.data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create new agent
|
||||||
|
const { createAgent } = await import("@/services/agentService");
|
||||||
|
const created = await createAgent(sanitizedData as any);
|
||||||
|
|
||||||
|
const res = await listAgents(clientId, 0, 100, selectedFolderId || undefined);
|
||||||
|
const filteredAgents = res.data.filter((agent: Agent) => agent.id !== currentAgentId);
|
||||||
|
setAllAgents(filteredAgents);
|
||||||
|
setAgents(filteredAgents);
|
||||||
|
|
||||||
|
if (created.data) {
|
||||||
|
handleSelectAgent(created.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAgentDialogOpen(false);
|
||||||
|
setIsEditMode(false);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error saving agent:", e);
|
||||||
|
setIsAgentDialogOpen(false);
|
||||||
|
setIsEditMode(false);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFolderChange = (value: string) => {
|
||||||
|
setSelectedFolderId(value === "all" ? null : value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAgentTypeChange = (value: string) => {
|
||||||
|
setSelectedAgentType(value === "all" ? null : value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFolderNameById = (id: string) => {
|
||||||
|
const folder = folders.find((f) => f.id === id);
|
||||||
|
return folder?.name || id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditAgent = () => {
|
||||||
|
if (!node.data.agent) return;
|
||||||
|
|
||||||
|
setNewAgent({
|
||||||
|
...node.data.agent,
|
||||||
|
client_id: clientId || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsEditMode(true);
|
||||||
|
setIsAgentDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isAgentDialogOpen && (
|
||||||
|
<GlobalAgentForm
|
||||||
|
open={isAgentDialogOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setIsAgentDialogOpen(open);
|
||||||
|
if (!open) setIsEditMode(false);
|
||||||
|
}}
|
||||||
|
initialValues={newAgent}
|
||||||
|
apiKeys={apiKeys}
|
||||||
|
availableModels={availableModels}
|
||||||
|
availableMCPs={availableMCPs}
|
||||||
|
agents={allAgents}
|
||||||
|
onOpenApiKeysDialog={() => {}}
|
||||||
|
onOpenMCPDialog={() => {}}
|
||||||
|
onOpenCustomMCPDialog={() => {}}
|
||||||
|
onSave={handleSaveAgent}
|
||||||
|
isLoading={isLoading}
|
||||||
|
getAgentNameById={(id) => allAgents.find((a) => a.id === id)?.name || id}
|
||||||
|
clientId={clientId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Agent Test Chat Modal - moved outside of nested divs to render properly */}
|
||||||
|
{isTestModalOpen && node.data.agent && (
|
||||||
|
<AgentTestChatModal
|
||||||
|
open={isTestModalOpen}
|
||||||
|
onOpenChange={setIsTestModalOpen}
|
||||||
|
agent={node.data.agent}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="p-4 border-b border-neutral-700 flex-shrink-0">
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-neutral-500" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search agents..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-9 bg-neutral-800 border-neutral-700 text-neutral-200 focus-visible:ring-emerald-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Select
|
||||||
|
value={selectedFolderId ? selectedFolderId : "all"}
|
||||||
|
onValueChange={handleFolderChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full h-9 bg-neutral-800 border-neutral-700 text-neutral-200 focus:ring-emerald-500 focus:ring-offset-0">
|
||||||
|
<SelectValue placeholder="All folders" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||||
|
<SelectItem value="all">All folders</SelectItem>
|
||||||
|
{folders.map((folder) => (
|
||||||
|
<SelectItem key={folder.id} value={folder.id}>
|
||||||
|
{folder.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<Select
|
||||||
|
value={selectedAgentType ? selectedAgentType : "all"}
|
||||||
|
onValueChange={handleAgentTypeChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full h-9 bg-neutral-800 border-neutral-700 text-neutral-200 focus:ring-emerald-500 focus:ring-offset-0">
|
||||||
|
<SelectValue placeholder="All types" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||||
|
<SelectItem value="all">All types</SelectItem>
|
||||||
|
{agentTypes.map((type) => (
|
||||||
|
<SelectItem key={type} value={type}>
|
||||||
|
{getAgentTypeName(type)}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col flex-1 min-h-0">
|
||||||
|
<div className="px-4 pt-4 pb-2 flex items-center justify-between flex-shrink-0">
|
||||||
|
<h3 className="text-md font-medium text-neutral-200">
|
||||||
|
{searchQuery ? "Search Results" : "Select an Agent"}
|
||||||
|
</h3>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 bg-emerald-800 hover:bg-emerald-700 border-emerald-700 text-emerald-100"
|
||||||
|
onClick={() => {
|
||||||
|
setNewAgent({
|
||||||
|
id: "",
|
||||||
|
name: "",
|
||||||
|
client_id: clientId || "",
|
||||||
|
type: "llm",
|
||||||
|
model: "",
|
||||||
|
config: {},
|
||||||
|
description: "",
|
||||||
|
});
|
||||||
|
setIsEditMode(false);
|
||||||
|
setIsAgentDialogOpen(true);
|
||||||
|
}}
|
||||||
|
aria-label="New Agent"
|
||||||
|
>
|
||||||
|
<Plus size={14} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto px-4 scrollbar-hide">
|
||||||
|
<div className="space-y-2 pr-2">
|
||||||
|
{agents.length > 0 ? (
|
||||||
|
agents.map((agent) => (
|
||||||
|
<div
|
||||||
|
key={agent.id}
|
||||||
|
className={`p-3 rounded-md cursor-pointer transition-colors group relative ${
|
||||||
|
node.data.agent?.id === agent.id
|
||||||
|
? "bg-emerald-800/20 border border-emerald-600/40"
|
||||||
|
: "bg-neutral-800 hover:bg-neutral-700 border border-transparent"
|
||||||
|
}`}
|
||||||
|
onClick={() => handleSelectAgent(agent)}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className="bg-neutral-700 rounded-full p-1.5 flex-shrink-0">
|
||||||
|
<User size={18} className="text-neutral-300" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="font-medium text-neutral-200 truncate">{agent.name}</h3>
|
||||||
|
<div
|
||||||
|
className="ml-auto text-neutral-400 opacity-0 group-hover:opacity-100 hover:text-yellow-500 transition-colors p-1 rounded hover:bg-yellow-900/20"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setNewAgent({
|
||||||
|
...agent,
|
||||||
|
client_id: clientId || "",
|
||||||
|
});
|
||||||
|
setIsEditMode(true);
|
||||||
|
setIsAgentDialogOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
|
||||||
|
<path d="m15 5 4 4" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs bg-neutral-700 text-emerald-400 border-neutral-600"
|
||||||
|
>
|
||||||
|
{getAgentTypeName(agent.type)}
|
||||||
|
</Badge>
|
||||||
|
{agent.model && (
|
||||||
|
<span className="text-xs text-neutral-400">{agent.model}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{agent.description && (
|
||||||
|
<p className="text-sm text-neutral-400 mt-1.5 line-clamp-2">
|
||||||
|
{agent.description.slice(0, 30)} {agent.description.length > 30 ? "..." : ""}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-4 text-neutral-400">
|
||||||
|
No agents found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{node.data.agent && (
|
||||||
|
<div className="p-4 border-t border-neutral-700 flex-shrink-0">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-md font-medium text-neutral-200">Selected Agent</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 bg-neutral-700 hover:bg-neutral-600 border-neutral-600 text-neutral-200"
|
||||||
|
onClick={() => {
|
||||||
|
handleUpdateNode({
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
agent: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
aria-label="Clear agent"
|
||||||
|
>
|
||||||
|
<X size={14} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 bg-neutral-700 hover:bg-neutral-600 border-neutral-600 text-neutral-200"
|
||||||
|
onClick={handleEditAgent}
|
||||||
|
aria-label="Edit agent"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
|
||||||
|
<path d="m15 5 4 4" />
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 bg-emerald-800 hover:bg-emerald-700 border-emerald-700 text-emerald-100"
|
||||||
|
onClick={() => setIsTestModalOpen(true)}
|
||||||
|
aria-label="Test agent"
|
||||||
|
>
|
||||||
|
<PlayIcon size={14} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-md bg-emerald-800/20 border border-emerald-600/40">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className="bg-emerald-900/50 rounded-full p-1.5 flex-shrink-0">
|
||||||
|
<User size={18} className="text-emerald-300" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="font-medium text-neutral-200 truncate">{node.data.agent.name}</h3>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs bg-emerald-900/50 text-emerald-400 border-emerald-700/50"
|
||||||
|
>
|
||||||
|
{getAgentTypeName(node.data.agent.type)}
|
||||||
|
</Badge>
|
||||||
|
{node.data.agent.model && (
|
||||||
|
<span className="text-xs text-neutral-400">{node.data.agent.model}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{node.data.agent.description && (
|
||||||
|
<p className="text-sm text-neutral-400 mt-1.5 line-clamp-2">
|
||||||
|
{node.data.agent.description.slice(0, 30)} {node.data.agent.description.length > 30 ? "..." : ""}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/nodes/components/agent/AgentNode.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { Handle, NodeProps, Position, useEdges } from "@xyflow/react";
|
||||||
|
import { MessageCircle, User, Code, ExternalLink, Workflow, GitBranch, RefreshCw, BookOpenCheck, ArrowRight } from "lucide-react";
|
||||||
|
import { Agent } from "@/types/agent";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import { BaseNode } from "../../BaseNode";
|
||||||
|
|
||||||
|
export function AgentNode(props: NodeProps) {
|
||||||
|
const { selected, data } = props;
|
||||||
|
|
||||||
|
const edges = useEdges();
|
||||||
|
|
||||||
|
const isHandleConnected = (handleId: string) => {
|
||||||
|
return edges.some(
|
||||||
|
(edge) => edge.source === props.id && edge.sourceHandle === handleId
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isBottomHandleConnected = isHandleConnected("bottom-handle");
|
||||||
|
|
||||||
|
const agent = data.agent as Agent | undefined;
|
||||||
|
const isExecuting = data.isExecuting as boolean | undefined;
|
||||||
|
|
||||||
|
const getAgentTypeName = (type: string) => {
|
||||||
|
const agentTypes: Record<string, string> = {
|
||||||
|
llm: "LLM Agent",
|
||||||
|
a2a: "A2A Agent",
|
||||||
|
sequential: "Sequential Agent",
|
||||||
|
parallel: "Parallel Agent",
|
||||||
|
loop: "Loop Agent",
|
||||||
|
workflow: "Workflow Agent",
|
||||||
|
task: "Task Agent",
|
||||||
|
};
|
||||||
|
return agentTypes[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAgentTypeIcon = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case "llm":
|
||||||
|
return <Code className="h-4 w-4 text-green-400" />;
|
||||||
|
case "a2a":
|
||||||
|
return <ExternalLink className="h-4 w-4 text-indigo-400" />;
|
||||||
|
case "sequential":
|
||||||
|
return <Workflow className="h-4 w-4 text-yellow-400" />;
|
||||||
|
case "parallel":
|
||||||
|
return <GitBranch className="h-4 w-4 text-purple-400" />;
|
||||||
|
case "loop":
|
||||||
|
return <RefreshCw className="h-4 w-4 text-orange-400" />;
|
||||||
|
case "workflow":
|
||||||
|
return <Workflow className="h-4 w-4 text-blue-400" />;
|
||||||
|
case "task":
|
||||||
|
return <BookOpenCheck className="h-4 w-4 text-red-400" />;
|
||||||
|
default:
|
||||||
|
return <User className="h-4 w-4 text-neutral-400" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getModelBadgeColor = (model: string) => {
|
||||||
|
if (model?.includes('gpt-4')) return 'bg-green-900/30 text-green-400 border-green-600/30';
|
||||||
|
if (model?.includes('gpt-3')) return 'bg-yellow-900/30 text-yellow-400 border-yellow-600/30';
|
||||||
|
if (model?.includes('claude')) return 'bg-orange-900/30 text-orange-400 border-orange-600/30';
|
||||||
|
if (model?.includes('gemini')) return 'bg-blue-900/30 text-blue-400 border-blue-600/30';
|
||||||
|
return 'bg-neutral-800 text-neutral-400 border-neutral-600/50';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseNode hasTarget={true} selected={selected || false} borderColor="blue" isExecuting={isExecuting}>
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-900/40 shadow-sm">
|
||||||
|
<User className="h-5 w-5 text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-medium text-blue-400">
|
||||||
|
{data.label as string}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{agent ? (
|
||||||
|
<div className="mb-3 rounded-lg border border-blue-700/40 bg-blue-950/10 p-3 transition-all duration-200 hover:border-blue-600/50 hover:bg-blue-900/10">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{getAgentTypeIcon(agent.type)}
|
||||||
|
<span className="ml-1.5 font-medium text-white">{agent.name}</span>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="px-1.5 py-0 text-xs bg-blue-900/30 text-blue-400 border-blue-700/40"
|
||||||
|
>
|
||||||
|
{getAgentTypeName(agent.type)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{agent.model && (
|
||||||
|
<div className="mt-2 flex items-center">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={cn("px-1.5 py-0 text-xs", getModelBadgeColor(agent.model))}
|
||||||
|
>
|
||||||
|
{agent.model}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{agent.description && (
|
||||||
|
<p className="mt-2 text-xs text-neutral-400 line-clamp-2">
|
||||||
|
{agent.description.slice(0, 30)} {agent.description.length > 30 && '...'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mb-3 flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-blue-700/40 bg-blue-950/10 p-5 text-center transition-all duration-200 hover:border-blue-600/50 hover:bg-blue-900/20">
|
||||||
|
<User className="h-8 w-8 text-blue-700/50 mb-2" />
|
||||||
|
<p className="text-blue-400">Select an agent</p>
|
||||||
|
<p className="mt-1 text-xs text-neutral-500">Click to configure</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-2 flex items-center justify-end text-sm text-neutral-400 transition-colors">
|
||||||
|
<div className="flex items-center space-x-1 rounded-md py-1 px-2">
|
||||||
|
<span>Next step</span>
|
||||||
|
<ArrowRight className="h-3.5 w-3.5" />
|
||||||
|
</div>
|
||||||
|
<Handle
|
||||||
|
className={cn(
|
||||||
|
"!w-3 !h-3 !rounded-full transition-all duration-300",
|
||||||
|
isBottomHandleConnected ? "!bg-blue-500 !border-blue-400" : "!bg-neutral-400 !border-neutral-500",
|
||||||
|
selected && isBottomHandleConnected && "!bg-blue-400 !border-blue-300"
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
right: "-8px",
|
||||||
|
top: "calc(100% - 25px)",
|
||||||
|
}}
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
id="bottom-handle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</BaseNode>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,470 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/nodes/components/agent/AgentTestChatModal.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
import { useState, useCallback, useEffect, useRef } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { useAgentWebSocket } from "@/hooks/use-agent-webSocket";
|
||||||
|
import { getAccessTokenFromCookie, cn } from "@/lib/utils";
|
||||||
|
import { Agent } from "@/types/agent";
|
||||||
|
import { ChatInput } from "@/app/chat/components/ChatInput";
|
||||||
|
import { ChatMessage as ChatMessageComponent } from "@/app/chat/components/ChatMessage";
|
||||||
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
|
import { ChatPart } from "@/services/sessionService";
|
||||||
|
import { FileData } from "@/lib/file-utils";
|
||||||
|
import { X, User, Bot, Zap, MessageSquare, Loader2, Code, ExternalLink, Workflow, RefreshCw } from "lucide-react";
|
||||||
|
|
||||||
|
interface FunctionMessageContent {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
author?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatMessage {
|
||||||
|
id: string;
|
||||||
|
content: any;
|
||||||
|
author: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AgentTestChatModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
agent: Agent;
|
||||||
|
canvasRef?: React.RefObject<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgentTestChatModal({ open, onOpenChange, agent, canvasRef }: AgentTestChatModalProps) {
|
||||||
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
|
const [isSending, setIsSending] = useState(false);
|
||||||
|
const [expandedFunctions, setExpandedFunctions] = useState<Record<string, boolean>>({});
|
||||||
|
const [isInitializing, setIsInitializing] = useState(true);
|
||||||
|
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const user = typeof window !== "undefined" ? JSON.parse(localStorage.getItem("user") || "{}") : {};
|
||||||
|
const clientId = user?.client_id || "test";
|
||||||
|
|
||||||
|
const generateExternalId = () => {
|
||||||
|
const now = new Date();
|
||||||
|
return (
|
||||||
|
now.getFullYear().toString() +
|
||||||
|
(now.getMonth() + 1).toString().padStart(2, "0") +
|
||||||
|
now.getDate().toString().padStart(2, "0") +
|
||||||
|
now.getHours().toString().padStart(2, "0") +
|
||||||
|
now.getMinutes().toString().padStart(2, "0") +
|
||||||
|
now.getSeconds().toString().padStart(2, "0") +
|
||||||
|
now.getMilliseconds().toString().padStart(3, "0")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [externalId, setExternalId] = useState(generateExternalId());
|
||||||
|
const jwt = getAccessTokenFromCookie();
|
||||||
|
|
||||||
|
const onEvent = useCallback((event: any) => {
|
||||||
|
setMessages((prev) => [...prev, event]);
|
||||||
|
|
||||||
|
// Check if the message comes from a workflow node and highlight the node
|
||||||
|
// only if the canvasRef is available (called from Test Workflow on the main page)
|
||||||
|
if (event.author && event.author.startsWith('workflow-node:') && canvasRef?.current) {
|
||||||
|
const nodeId = event.author.split(':')[1];
|
||||||
|
canvasRef.current.setActiveExecutionNodeId(nodeId);
|
||||||
|
}
|
||||||
|
}, [canvasRef]);
|
||||||
|
|
||||||
|
const onTurnComplete = useCallback(() => {
|
||||||
|
setIsSending(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { sendMessage: wsSendMessage, disconnect } = useAgentWebSocket({
|
||||||
|
agentId: agent.id,
|
||||||
|
externalId,
|
||||||
|
jwt,
|
||||||
|
onEvent,
|
||||||
|
onTurnComplete,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle ESC key to close the panel
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape" && open) {
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [onOpenChange, open]);
|
||||||
|
|
||||||
|
// Show initialization state for better UX
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setIsInitializing(true);
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsInitializing(false);
|
||||||
|
}, 1200);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [open, externalId]);
|
||||||
|
|
||||||
|
const handleRestartChat = () => {
|
||||||
|
if (disconnect) disconnect();
|
||||||
|
setMessages([]);
|
||||||
|
setExpandedFunctions({});
|
||||||
|
setExternalId(generateExternalId());
|
||||||
|
setIsInitializing(true);
|
||||||
|
|
||||||
|
// Short delay to show the initialization status
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsInitializing(false);
|
||||||
|
}, 1200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendMessageWithFiles = (message: string, files?: FileData[]) => {
|
||||||
|
if ((!message.trim() && (!files || files.length === 0))) return;
|
||||||
|
setIsSending(true);
|
||||||
|
|
||||||
|
const messageParts: ChatPart[] = [];
|
||||||
|
|
||||||
|
if (message.trim()) {
|
||||||
|
messageParts.push({ text: message });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
files.forEach(file => {
|
||||||
|
messageParts.push({
|
||||||
|
inline_data: {
|
||||||
|
data: file.data,
|
||||||
|
mime_type: file.content_type,
|
||||||
|
metadata: {
|
||||||
|
filename: file.filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessages((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
id: `temp-${Date.now()}`,
|
||||||
|
content: {
|
||||||
|
parts: messageParts,
|
||||||
|
role: "user"
|
||||||
|
},
|
||||||
|
author: "user",
|
||||||
|
timestamp: Date.now() / 1000,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
wsSendMessage(message, files);
|
||||||
|
};
|
||||||
|
|
||||||
|
const containsMarkdown = (text: string): boolean => {
|
||||||
|
if (!text || text.length < 3) return false;
|
||||||
|
const markdownPatterns = [
|
||||||
|
/[*_]{1,2}[^*_]+[*_]{1,2}/, // bold/italic
|
||||||
|
/\[[^\]]+\]\([^)]+\)/, // links
|
||||||
|
/^#{1,6}\s/m, // headers
|
||||||
|
/^[-*+]\s/m, // unordered lists
|
||||||
|
/^[0-9]+\.\s/m, // ordered lists
|
||||||
|
/^>\s/m, // block quotes
|
||||||
|
/`[^`]+`/, // inline code
|
||||||
|
/```[\s\S]*?```/, // code blocks
|
||||||
|
/^\|(.+\|)+$/m, // tables
|
||||||
|
/!\[[^\]]*\]\([^)]+\)/, // images
|
||||||
|
];
|
||||||
|
return markdownPatterns.some((pattern) => pattern.test(text));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMessageText = (message: ChatMessage): string | FunctionMessageContent => {
|
||||||
|
const author = message.author;
|
||||||
|
const parts = message.content.parts;
|
||||||
|
if (!parts || parts.length === 0) return "Empty content";
|
||||||
|
const functionCallPart = parts.find((part: any) => part.functionCall || part.function_call);
|
||||||
|
const functionResponsePart = parts.find((part: any) => part.functionResponse || part.function_response);
|
||||||
|
|
||||||
|
const inlineDataParts = parts.filter((part: any) => part.inline_data);
|
||||||
|
|
||||||
|
if (functionCallPart) {
|
||||||
|
const funcCall = functionCallPart.functionCall || functionCallPart.function_call || {};
|
||||||
|
const args = funcCall.args || {};
|
||||||
|
const name = funcCall.name || "unknown";
|
||||||
|
const id = funcCall.id || "no-id";
|
||||||
|
return {
|
||||||
|
author,
|
||||||
|
title: `📞 Function call: ${name}`,
|
||||||
|
content: `ID: ${id}\nArgs: ${Object.keys(args).length > 0 ? `\n${JSON.stringify(args, null, 2)}` : "{}"}`,
|
||||||
|
} as FunctionMessageContent;
|
||||||
|
}
|
||||||
|
if (functionResponsePart) {
|
||||||
|
const funcResponse = functionResponsePart.functionResponse || functionResponsePart.function_response || {};
|
||||||
|
const response = funcResponse.response || {};
|
||||||
|
const name = funcResponse.name || "unknown";
|
||||||
|
const id = funcResponse.id || "no-id";
|
||||||
|
const status = response.status || "unknown";
|
||||||
|
const statusEmoji = status === "error" ? "❌" : "✅";
|
||||||
|
let resultText = "";
|
||||||
|
if (status === "error") {
|
||||||
|
resultText = `Error: ${response.error_message || "Unknown error"}`;
|
||||||
|
} else if (response.report) {
|
||||||
|
resultText = `Result: ${response.report}`;
|
||||||
|
} else if (response.result && response.result.content) {
|
||||||
|
const content = response.result.content;
|
||||||
|
if (Array.isArray(content) && content.length > 0 && content[0].text) {
|
||||||
|
try {
|
||||||
|
const textContent = content[0].text;
|
||||||
|
const parsedJson = JSON.parse(textContent);
|
||||||
|
resultText = `Result: \n${JSON.stringify(parsedJson, null, 2)}`;
|
||||||
|
} catch (e) {
|
||||||
|
resultText = `Result: ${content[0].text}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resultText = `Result:\n${JSON.stringify(response, null, 2)}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resultText = `Result:\n${JSON.stringify(response, null, 2)}`;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
author,
|
||||||
|
title: `${statusEmoji} Function response: ${name}`,
|
||||||
|
content: `ID: ${id}\n${resultText}`,
|
||||||
|
} as FunctionMessageContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length === 1 && parts[0].text) {
|
||||||
|
return {
|
||||||
|
author,
|
||||||
|
content: parts[0].text,
|
||||||
|
title: "Message",
|
||||||
|
} as FunctionMessageContent;
|
||||||
|
}
|
||||||
|
const textParts = parts.filter((part: any) => part.text).map((part: any) => part.text).filter((text: string) => text);
|
||||||
|
if (textParts.length > 0) {
|
||||||
|
return {
|
||||||
|
author,
|
||||||
|
content: textParts.join("\n\n"),
|
||||||
|
title: "Message",
|
||||||
|
} as FunctionMessageContent;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.stringify(parts, null, 2).replace(/\\n/g, "\n");
|
||||||
|
} catch (error) {
|
||||||
|
return "Unable to interpret message content";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleFunctionExpansion = (messageId: string) => {
|
||||||
|
setExpandedFunctions((prev) => ({ ...prev, [messageId]: !prev[messageId] }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAgentTypeIcon = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case "llm":
|
||||||
|
return <Code className="h-4 w-4 text-green-400" />;
|
||||||
|
case "a2a":
|
||||||
|
return <ExternalLink className="h-4 w-4 text-indigo-400" />;
|
||||||
|
case "sequential":
|
||||||
|
case "workflow":
|
||||||
|
return <Workflow className="h-4 w-4 text-blue-400" />;
|
||||||
|
default:
|
||||||
|
return <Bot className="h-4 w-4 text-emerald-400" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Scroll to bottom whenever messages change
|
||||||
|
useEffect(() => {
|
||||||
|
if (messagesContainerRef.current) {
|
||||||
|
messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
// Use React Portal to render directly to document body, bypassing all parent containers
|
||||||
|
const modalContent = (
|
||||||
|
<>
|
||||||
|
{/* Overlay for mobile */}
|
||||||
|
<div
|
||||||
|
className="md:hidden fixed inset-0 bg-black bg-opacity-50 z-[15] transition-opacity duration-300"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Side panel */}
|
||||||
|
<div
|
||||||
|
className="fixed right-0 top-0 z-[1000] h-full w-[450px] bg-gradient-to-b from-neutral-900 to-neutral-950 border-l border-neutral-800 shadow-2xl flex flex-col transition-all duration-300 ease-in-out transform"
|
||||||
|
style={{
|
||||||
|
transform: open ? 'translateX(0)' : 'translateX(100%)',
|
||||||
|
boxShadow: '0 0 25px rgba(0, 0, 0, 0.3)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex-shrink-0 p-5 bg-gradient-to-r from-neutral-900 to-neutral-800 border-b border-neutral-800">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center mb-1">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-emerald-600 to-emerald-900 flex items-center justify-center shadow-lg mr-3">
|
||||||
|
{getAgentTypeIcon(agent.type)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-white">{agent.name}</h2>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<Badge
|
||||||
|
className="bg-emerald-900/40 text-emerald-400 border border-emerald-700/50 px-2"
|
||||||
|
>
|
||||||
|
{agent.type.toUpperCase()} Agent
|
||||||
|
</Badge>
|
||||||
|
{agent.model && (
|
||||||
|
<span className="text-xs text-neutral-400 bg-neutral-800/60 px-2 py-0.5 rounded-md">
|
||||||
|
{agent.model}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={handleRestartChat}
|
||||||
|
className="p-1.5 rounded-full hover:bg-neutral-700/50 text-neutral-400 hover:text-white transition-colors group relative"
|
||||||
|
title="Restart chat"
|
||||||
|
disabled={isInitializing}
|
||||||
|
>
|
||||||
|
<RefreshCw size={18} className={isInitializing ? "animate-spin text-emerald-400" : ""} />
|
||||||
|
<span className="absolute -bottom-8 right-0 bg-neutral-800 text-neutral-200 text-xs rounded py-1 px-2 opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap">
|
||||||
|
Restart chat
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="p-1.5 rounded-full hover:bg-neutral-700/50 text-neutral-400 hover:text-white transition-colors group relative"
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
<span className="absolute -bottom-8 right-0 bg-neutral-800 text-neutral-200 text-xs rounded py-1 px-2 opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap">
|
||||||
|
Close
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{agent.description && (
|
||||||
|
<div className="mt-3 text-sm text-neutral-400 bg-neutral-800/30 p-3 rounded-md border border-neutral-800">
|
||||||
|
{agent.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chat content */}
|
||||||
|
<div className="flex-1 overflow-y-auto overflow-x-hidden p-3 bg-gradient-to-b from-neutral-900/50 to-neutral-950" ref={messagesContainerRef}>
|
||||||
|
{isInitializing ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full">
|
||||||
|
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-emerald-500 to-emerald-700 flex items-center justify-center shadow-lg mb-4 animate-pulse">
|
||||||
|
<Zap className="h-5 w-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<p className="text-neutral-400 mb-2">Initializing connection...</p>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-bounce"
|
||||||
|
style={{ animationDelay: '0ms' }}></span>
|
||||||
|
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-bounce"
|
||||||
|
style={{ animationDelay: '150ms' }}></span>
|
||||||
|
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-bounce"
|
||||||
|
style={{ animationDelay: '300ms' }}></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : messages.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full text-center px-6">
|
||||||
|
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-blue-500/20 to-emerald-500/20 flex items-center justify-center shadow-lg mb-5 border border-emerald-500/30">
|
||||||
|
<MessageSquare className="h-6 w-6 text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium text-neutral-300 mb-2">Start the conversation</h3>
|
||||||
|
<p className="text-neutral-500 text-sm max-w-xs">
|
||||||
|
Type a message below to begin chatting with {agent.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4 w-full max-w-full">
|
||||||
|
{messages.map((message) => {
|
||||||
|
const messageContent = getMessageText(message);
|
||||||
|
const agentColor = message.author === "user" ? "bg-emerald-500" : "bg-gradient-to-br from-neutral-800 to-neutral-900";
|
||||||
|
const isExpanded = expandedFunctions[message.id] || false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChatMessageComponent
|
||||||
|
key={message.id}
|
||||||
|
message={message}
|
||||||
|
agentColor={agentColor}
|
||||||
|
isExpanded={isExpanded}
|
||||||
|
toggleExpansion={toggleFunctionExpansion}
|
||||||
|
containsMarkdown={containsMarkdown}
|
||||||
|
messageContent={messageContent}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{isSending && (
|
||||||
|
<div className="flex justify-start">
|
||||||
|
<div className="flex gap-3 max-w-[80%]">
|
||||||
|
<Avatar
|
||||||
|
className="bg-gradient-to-br from-purple-600 to-purple-800 shadow-md border-0"
|
||||||
|
>
|
||||||
|
<AvatarFallback className="bg-transparent">
|
||||||
|
<Bot className="h-4 w-4 text-white" />
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="rounded-lg p-3 bg-gradient-to-br from-neutral-800 to-neutral-900 border border-neutral-700/50">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<div className="h-2 w-2 rounded-full bg-emerald-400 animate-bounce"></div>
|
||||||
|
<div className="h-2 w-2 rounded-full bg-emerald-400 animate-bounce [animation-delay:0.2s]"></div>
|
||||||
|
<div className="h-2 w-2 rounded-full bg-emerald-400 animate-bounce [animation-delay:0.4s]"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message input */}
|
||||||
|
<div className="p-3 border-t border-neutral-800 bg-neutral-900">
|
||||||
|
<ChatInput
|
||||||
|
onSendMessage={handleSendMessageWithFiles}
|
||||||
|
isLoading={isSending}
|
||||||
|
placeholder="Type your message..."
|
||||||
|
autoFocus={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use createPortal to render the modal directly to the document body
|
||||||
|
return typeof document !== 'undefined'
|
||||||
|
? createPortal(modalContent, document.body)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/nodes/components/agent/styles.css │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
|
||||||
|
.markdown-content {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
hyphens: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content code {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content p {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content table {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-wrap-anywhere {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/nodes/components/condition/ConditionDialog.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { useState } from "react";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
import { ConditionType, ConditionTypeEnum } from "../../nodeFunctions";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Filter, ArrowRight } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
|
const conditionTypes = [
|
||||||
|
{
|
||||||
|
id: "previous-output",
|
||||||
|
name: "Previous output",
|
||||||
|
description: "Validate the result returned by the previous node",
|
||||||
|
icon: <Filter className="h-5 w-5 text-blue-400" />,
|
||||||
|
color: "bg-blue-900/30 border-blue-700/50",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const operators = [
|
||||||
|
{ value: "is_defined", label: "is defined" },
|
||||||
|
{ value: "is_not_defined", label: "is not defined" },
|
||||||
|
{ value: "equals", label: "is equal to" },
|
||||||
|
{ value: "not_equals", label: "is not equal to" },
|
||||||
|
{ value: "contains", label: "contains" },
|
||||||
|
{ value: "not_contains", label: "does not contain" },
|
||||||
|
{ value: "starts_with", label: "starts with" },
|
||||||
|
{ value: "ends_with", label: "ends with" },
|
||||||
|
{ value: "greater_than", label: "is greater than" },
|
||||||
|
{ value: "greater_than_or_equal", label: "is greater than or equal to" },
|
||||||
|
{ value: "less_than", label: "is less than" },
|
||||||
|
{ value: "less_than_or_equal", label: "is less than or equal to" },
|
||||||
|
{ value: "matches", label: "matches the regex" },
|
||||||
|
{ value: "not_matches", label: "does not match the regex" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const outputFields = [
|
||||||
|
{ value: "content", label: "Content" },
|
||||||
|
{ value: "status", label: "Status" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function ConditionDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
selectedNode,
|
||||||
|
handleUpdateNode,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
selectedNode: any;
|
||||||
|
handleUpdateNode: any;
|
||||||
|
}) {
|
||||||
|
const [selectedType, setSelectedType] = useState("previous-output");
|
||||||
|
const [selectedField, setSelectedField] = useState(outputFields[0].value);
|
||||||
|
const [selectedOperator, setSelectedOperator] = useState(operators[0].value);
|
||||||
|
const [comparisonValue, setComparisonValue] = useState("");
|
||||||
|
|
||||||
|
const handleConditionSave = (condition: ConditionType) => {
|
||||||
|
const newConditions = selectedNode.data.conditions
|
||||||
|
? [...selectedNode.data.conditions]
|
||||||
|
: [];
|
||||||
|
newConditions.push(condition);
|
||||||
|
|
||||||
|
const updatedNode = {
|
||||||
|
...selectedNode,
|
||||||
|
data: {
|
||||||
|
...selectedNode.data,
|
||||||
|
conditions: newConditions,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
handleUpdateNode(updatedNode);
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOperatorLabel = (value: string) => {
|
||||||
|
return operators.find(op => op.value === value)?.label || value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFieldLabel = (value: string) => {
|
||||||
|
return outputFields.find(field => field.value === value)?.label || value;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="bg-neutral-800 border-neutral-700 text-neutral-200 sm:max-w-[650px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add New Condition</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="grid gap-6 py-4">
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<Label className="text-sm font-medium">Condition Type</Label>
|
||||||
|
<div className="grid grid-cols-1 gap-2">
|
||||||
|
{conditionTypes.map((type) => (
|
||||||
|
<div
|
||||||
|
key={type.id}
|
||||||
|
className={`flex items-center space-x-3 rounded-md border p-3 cursor-pointer transition-all ${
|
||||||
|
selectedType === type.id
|
||||||
|
? "bg-blue-900/30 border-blue-600"
|
||||||
|
: "border-neutral-700 hover:border-blue-700/50 hover:bg-neutral-700/50"
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedType(type.id)}
|
||||||
|
>
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-blue-900/40">
|
||||||
|
{type.icon}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-medium">{type.name}</h4>
|
||||||
|
<p className="text-xs text-neutral-400">{type.description}</p>
|
||||||
|
</div>
|
||||||
|
{selectedType === type.id && (
|
||||||
|
<Badge className="bg-blue-600 text-neutral-100">Selected</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-sm font-medium">Configuration</Label>
|
||||||
|
{selectedType === "previous-output" && (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-neutral-400">
|
||||||
|
<span>Output field</span>
|
||||||
|
<ArrowRight className="h-3 w-3" />
|
||||||
|
<span>Operator</span>
|
||||||
|
{!["is_defined", "is_not_defined"].includes(selectedOperator) && (
|
||||||
|
<>
|
||||||
|
<ArrowRight className="h-3 w-3" />
|
||||||
|
<span>Value</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedType === "previous-output" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="field">Output Field</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedField}
|
||||||
|
onValueChange={setSelectedField}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="field" className="bg-neutral-700 border-neutral-600">
|
||||||
|
<SelectValue placeholder="Select field" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-neutral-700 border-neutral-600">
|
||||||
|
{outputFields.map((field) => (
|
||||||
|
<SelectItem key={field.value} value={field.value}>
|
||||||
|
{field.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="operator">Operator</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedOperator}
|
||||||
|
onValueChange={setSelectedOperator}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="operator" className="bg-neutral-700 border-neutral-600">
|
||||||
|
<SelectValue placeholder="Select operator" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-neutral-700 border-neutral-600">
|
||||||
|
{operators.map((op) => (
|
||||||
|
<SelectItem key={op.value} value={op.value}>
|
||||||
|
{op.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!["is_defined", "is_not_defined"].includes(selectedOperator) && (
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="value">Comparison Value</Label>
|
||||||
|
<Input
|
||||||
|
id="value"
|
||||||
|
value={comparisonValue}
|
||||||
|
onChange={(e) => setComparisonValue(e.target.value)}
|
||||||
|
className="bg-neutral-700 border-neutral-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="rounded-md bg-neutral-700/50 border border-neutral-600 p-3 mt-4">
|
||||||
|
<div className="text-sm font-medium text-neutral-400 mb-1">Preview</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="text-blue-400 font-medium">{getFieldLabel(selectedField)}</span>
|
||||||
|
{" "}
|
||||||
|
<span className="text-neutral-300">{getOperatorLabel(selectedOperator)}</span>
|
||||||
|
{" "}
|
||||||
|
{!["is_defined", "is_not_defined"].includes(selectedOperator) && (
|
||||||
|
<span className="text-emerald-400 font-medium">"{comparisonValue || "(empty)"}"</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="border-neutral-600 text-neutral-200 hover:bg-neutral-700"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
handleConditionSave({
|
||||||
|
id: uuidv4(),
|
||||||
|
type: ConditionTypeEnum.PREVIOUS_OUTPUT,
|
||||||
|
data: {
|
||||||
|
field: selectedField,
|
||||||
|
operator: selectedOperator,
|
||||||
|
value: comparisonValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="bg-blue-700 hover:bg-blue-600 text-white"
|
||||||
|
>
|
||||||
|
Add Condition
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ConditionDialog };
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/nodes/components/condition/ConditionForm.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
import { ConditionType, ConditionTypeEnum } from "../../nodeFunctions";
|
||||||
|
import { ConditionDialog } from "./ConditionDialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Filter, Trash2, Plus } from "lucide-react";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
function ConditionForm({
|
||||||
|
selectedNode,
|
||||||
|
handleUpdateNode,
|
||||||
|
}: {
|
||||||
|
selectedNode: any;
|
||||||
|
handleUpdateNode: any;
|
||||||
|
setEdges: any;
|
||||||
|
setIsOpen: any;
|
||||||
|
setSelectedNode: any;
|
||||||
|
}) {
|
||||||
|
const [node, setNode] = useState(selectedNode);
|
||||||
|
|
||||||
|
const [conditions, setConditions] = useState<ConditionType[]>(
|
||||||
|
selectedNode.data.conditions || []
|
||||||
|
);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [deleteDialog, setDeleteDialog] = useState(false);
|
||||||
|
const [conditionToDelete, setConditionToDelete] =
|
||||||
|
useState<ConditionType | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedNode) {
|
||||||
|
setNode(selectedNode);
|
||||||
|
setConditions(selectedNode.data.conditions || []);
|
||||||
|
}
|
||||||
|
}, [selectedNode]);
|
||||||
|
|
||||||
|
const handleDelete = (condition: ConditionType) => {
|
||||||
|
setConditionToDelete(condition);
|
||||||
|
setDeleteDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDelete = () => {
|
||||||
|
if (!conditionToDelete) return;
|
||||||
|
|
||||||
|
const newConditions = conditions.filter(
|
||||||
|
(c) => c.id !== conditionToDelete.id
|
||||||
|
);
|
||||||
|
setConditions(newConditions);
|
||||||
|
handleUpdateNode({
|
||||||
|
...node,
|
||||||
|
data: { ...node.data, conditions: newConditions },
|
||||||
|
});
|
||||||
|
setDeleteDialog(false);
|
||||||
|
setConditionToDelete(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderCondition = (condition: ConditionType) => {
|
||||||
|
if (condition.type === ConditionTypeEnum.PREVIOUS_OUTPUT) {
|
||||||
|
type OperatorType =
|
||||||
|
| "is_defined"
|
||||||
|
| "is_not_defined"
|
||||||
|
| "equals"
|
||||||
|
| "not_equals"
|
||||||
|
| "contains"
|
||||||
|
| "not_contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with"
|
||||||
|
| "greater_than"
|
||||||
|
| "greater_than_or_equal"
|
||||||
|
| "less_than"
|
||||||
|
| "less_than_or_equal"
|
||||||
|
| "matches"
|
||||||
|
| "not_matches";
|
||||||
|
|
||||||
|
const operatorText: Record<OperatorType, string> = {
|
||||||
|
is_defined: "is defined",
|
||||||
|
is_not_defined: "is not defined",
|
||||||
|
equals: "is equal to",
|
||||||
|
not_equals: "is not equal to",
|
||||||
|
contains: "contains",
|
||||||
|
not_contains: "does not contain",
|
||||||
|
starts_with: "starts with",
|
||||||
|
ends_with: "ends with",
|
||||||
|
greater_than: "is greater than",
|
||||||
|
greater_than_or_equal: "is greater than or equal to",
|
||||||
|
less_than: "is less than",
|
||||||
|
less_than_or_equal: "is less than or equal to",
|
||||||
|
matches: "matches the regex",
|
||||||
|
not_matches: "does not match the regex",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={condition.id}
|
||||||
|
className="p-3 rounded-md cursor-pointer transition-colors bg-neutral-800 hover:bg-neutral-700 border border-neutral-700 mb-2 group"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className="bg-blue-900/50 rounded-full p-1.5 flex-shrink-0">
|
||||||
|
<Filter size={18} className="text-blue-300" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="font-medium text-neutral-200">Condition</h3>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleDelete(condition)}
|
||||||
|
className="h-7 w-7 text-neutral-400 opacity-0 group-hover:opacity-100 hover:text-red-500 hover:bg-red-900/20"
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs bg-blue-900/20 text-blue-400 border-blue-700/50"
|
||||||
|
>
|
||||||
|
Field
|
||||||
|
</Badge>
|
||||||
|
<span className="text-sm text-neutral-300 font-medium">{condition.data.field}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-1 mt-1.5">
|
||||||
|
<span className="text-sm text-neutral-400">{operatorText[condition.data.operator as OperatorType]}</span>
|
||||||
|
{!["is_defined", "is_not_defined"].includes(condition.data.operator) && (
|
||||||
|
<span className="text-sm font-medium text-emerald-400">
|
||||||
|
"{condition.data.value}"
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="p-4 border-b border-neutral-700 flex-shrink-0">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-md font-medium text-neutral-200">Logic Type</h3>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs bg-blue-900/20 text-blue-400 border-blue-700/50"
|
||||||
|
>
|
||||||
|
{node.data.type === "or" ? "ANY" : "ALL"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={node.data.type || "and"}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const updatedNode = {
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
type: value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setNode(updatedNode);
|
||||||
|
handleUpdateNode(updatedNode);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[120px] h-8 bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||||
|
<SelectValue placeholder="Select type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||||
|
<SelectItem value="and">ALL (AND)</SelectItem>
|
||||||
|
<SelectItem value="or">ANY (OR)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-neutral-400 mt-2">
|
||||||
|
{node.data.type === "or"
|
||||||
|
? "Any of the following conditions must be true to proceed."
|
||||||
|
: "All of the following conditions must be true to proceed."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto px-4 py-4 min-h-0">
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="text-md font-medium text-neutral-200">Conditions</h3>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 bg-blue-800/20 hover:bg-blue-700/30 border-blue-700/50 text-blue-300"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
<Plus size={14} className="mr-1" />
|
||||||
|
Add Condition
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{conditions.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{conditions.map((condition) => renderCondition(condition))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
className="flex flex-col items-center justify-center p-6 rounded-lg border-2 border-dashed border-neutral-700 hover:border-blue-600/50 hover:bg-neutral-800/50 transition-colors cursor-pointer text-center"
|
||||||
|
>
|
||||||
|
<Filter className="h-10 w-10 text-neutral-500 mb-2" />
|
||||||
|
<p className="text-neutral-400">No conditions yet</p>
|
||||||
|
<p className="text-sm text-neutral-500 mt-1">Click to add a condition</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ConditionDialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
selectedNode={selectedNode}
|
||||||
|
handleUpdateNode={handleUpdateNode}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Dialog open={deleteDialog} onOpenChange={setDeleteDialog}>
|
||||||
|
<DialogContent className="bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Confirm Delete</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="py-4">
|
||||||
|
<p>Are you sure you want to delete this condition?</p>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="border-neutral-600 text-neutral-300 hover:bg-neutral-700"
|
||||||
|
onClick={() => {
|
||||||
|
setDeleteDialog(false);
|
||||||
|
setConditionToDelete(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
className="bg-red-900 hover:bg-red-800 text-white"
|
||||||
|
onClick={confirmDelete}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ConditionForm };
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/nodes/components/condition/ConditionNode.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { Handle, Node, NodeProps, Position, useEdges } from "@xyflow/react";
|
||||||
|
import { FilterIcon, ArrowRight } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import { BaseNode } from "../../BaseNode";
|
||||||
|
import { ConditionType, ConditionTypeEnum } from "../../nodeFunctions";
|
||||||
|
|
||||||
|
export type ConditionNodeType = Node<
|
||||||
|
{
|
||||||
|
label?: string;
|
||||||
|
type?: "and" | "or";
|
||||||
|
conditions?: ConditionType[];
|
||||||
|
},
|
||||||
|
"condition-node"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type OperatorType =
|
||||||
|
| "is_defined"
|
||||||
|
| "is_not_defined"
|
||||||
|
| "equals"
|
||||||
|
| "not_equals"
|
||||||
|
| "contains"
|
||||||
|
| "not_contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with"
|
||||||
|
| "greater_than"
|
||||||
|
| "greater_than_or_equal"
|
||||||
|
| "less_than"
|
||||||
|
| "less_than_or_equal"
|
||||||
|
| "matches"
|
||||||
|
| "not_matches";
|
||||||
|
|
||||||
|
const operatorText: Record<OperatorType, string> = {
|
||||||
|
equals: "is equal to",
|
||||||
|
not_equals: "is not equal to",
|
||||||
|
contains: "contains",
|
||||||
|
not_contains: "does not contain",
|
||||||
|
starts_with: "starts with",
|
||||||
|
ends_with: "ends with",
|
||||||
|
greater_than: "is greater than",
|
||||||
|
greater_than_or_equal: "is greater than or equal to",
|
||||||
|
less_than: "is less than",
|
||||||
|
less_than_or_equal: "is less than or equal to",
|
||||||
|
matches: "matches the pattern",
|
||||||
|
not_matches: "does not match the pattern",
|
||||||
|
is_defined: "is defined",
|
||||||
|
is_not_defined: "is not defined",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ConditionNode(props: NodeProps) {
|
||||||
|
const { selected, data } = props;
|
||||||
|
const edges = useEdges();
|
||||||
|
const isExecuting = data.isExecuting as boolean | undefined;
|
||||||
|
|
||||||
|
const typeText = {
|
||||||
|
and: "all of the following conditions",
|
||||||
|
or: "any of the following conditions",
|
||||||
|
};
|
||||||
|
|
||||||
|
const isHandleConnected = (handleId: string) => {
|
||||||
|
return edges.some(
|
||||||
|
(edge) => edge.source === props.id && edge.sourceHandle === handleId,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isBottomHandleConnected = isHandleConnected("bottom-handle");
|
||||||
|
|
||||||
|
const conditions: ConditionType[] = data.conditions as ConditionType[];
|
||||||
|
// const statistics: StatisticType = data.statistics as StatisticType;
|
||||||
|
|
||||||
|
const renderCondition = (condition: ConditionType) => {
|
||||||
|
const isConnected = isHandleConnected(condition.id);
|
||||||
|
|
||||||
|
if (condition.type === ConditionTypeEnum.PREVIOUS_OUTPUT) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="mb-3 cursor-pointer rounded-lg border border-purple-700/40 bg-purple-950/10 p-3 text-left transition-all duration-200 hover:border-purple-600/50 hover:bg-purple-900/10"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="font-medium text-neutral-300">
|
||||||
|
O campo{" "}
|
||||||
|
<span className="font-semibold text-purple-400">
|
||||||
|
{condition.data.field}
|
||||||
|
</span>{" "}
|
||||||
|
<span className="text-neutral-400">
|
||||||
|
{operatorText[condition.data.operator as OperatorType]}
|
||||||
|
</span>{" "}
|
||||||
|
{!["is_defined", "is_not_defined"].includes(
|
||||||
|
condition.data.operator,
|
||||||
|
) && (
|
||||||
|
<span className="font-semibold text-green-400">
|
||||||
|
"{condition.data.value}"
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Handle
|
||||||
|
className={cn(
|
||||||
|
"!rounded-full transition-all duration-300",
|
||||||
|
isConnected ? "!bg-purple-500 !border-purple-400" : "!bg-neutral-400 !border-neutral-500"
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
top: "50%",
|
||||||
|
right: "-5px",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
height: "14px",
|
||||||
|
position: "relative",
|
||||||
|
width: "14px",
|
||||||
|
}}
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
id={condition.id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseNode hasTarget={true} selected={selected || false} borderColor="purple" isExecuting={isExecuting}>
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-purple-900/40 shadow-sm">
|
||||||
|
<FilterIcon className="h-5 w-5 text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-medium text-purple-400">
|
||||||
|
{data.label as string}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-neutral-400">
|
||||||
|
Matches {typeText[(data.type as "and" | "or") || "and"]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{conditions && conditions.length > 0 && Array.isArray(conditions) ? (
|
||||||
|
conditions.map((condition) => (
|
||||||
|
<div key={condition.id}>{renderCondition(condition)}</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="mb-3 flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-purple-700/40 bg-purple-950/10 p-5 text-center transition-all duration-200 hover:border-purple-600/50 hover:bg-purple-900/20">
|
||||||
|
<FilterIcon className="h-8 w-8 text-purple-700/50 mb-2" />
|
||||||
|
<p className="text-purple-400">No conditions configured</p>
|
||||||
|
<p className="mt-1 text-xs text-neutral-500">Click to add a condition</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-2 flex items-center justify-end text-sm text-neutral-400 transition-colors">
|
||||||
|
<div className="flex items-center space-x-1 rounded-md py-1 px-2">
|
||||||
|
<span>Next step</span>
|
||||||
|
<ArrowRight className="h-3.5 w-3.5" />
|
||||||
|
</div>
|
||||||
|
<Handle
|
||||||
|
className={cn(
|
||||||
|
"!w-3 !h-3 !rounded-full transition-all duration-300",
|
||||||
|
isBottomHandleConnected ? "!bg-purple-500 !border-purple-400" : "!bg-neutral-400 !border-neutral-500",
|
||||||
|
selected && isBottomHandleConnected && "!bg-purple-400 !border-purple-300"
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
right: "0px",
|
||||||
|
top: "calc(100% - 25px)",
|
||||||
|
}}
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
id="bottom-handle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</BaseNode>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @author: Victor Calazans - Implementation of Delay node form │
|
||||||
|
│ @file: /app/agents/workflows/nodes/components/delay/DelayForm.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Delay form developed by: Victor Calazans │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Delay form implementation date: May 17, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Dispatch, SetStateAction } from "react";
|
||||||
|
import { Clock, Trash2, Save, AlertCircle, HourglassIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
|
import { DelayType, DelayUnitEnum } from "../../nodeFunctions";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export function DelayForm({
|
||||||
|
selectedNode,
|
||||||
|
handleUpdateNode,
|
||||||
|
setEdges,
|
||||||
|
setIsOpen,
|
||||||
|
setSelectedNode,
|
||||||
|
}: {
|
||||||
|
selectedNode: any;
|
||||||
|
handleUpdateNode: (node: any) => void;
|
||||||
|
setEdges: any;
|
||||||
|
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setSelectedNode: Dispatch<SetStateAction<any>>;
|
||||||
|
}) {
|
||||||
|
const [delay, setDelay] = useState<DelayType>({
|
||||||
|
value: 1,
|
||||||
|
unit: DelayUnitEnum.SECONDS,
|
||||||
|
description: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedNode?.data?.delay) {
|
||||||
|
setDelay(selectedNode.data.delay);
|
||||||
|
}
|
||||||
|
}, [selectedNode]);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
handleUpdateNode({
|
||||||
|
...selectedNode,
|
||||||
|
data: {
|
||||||
|
...selectedNode.data,
|
||||||
|
delay,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
setEdges((edges: any) => {
|
||||||
|
return edges.filter(
|
||||||
|
(edge: any) => edge.source !== selectedNode.id && edge.target !== selectedNode.id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsOpen(false);
|
||||||
|
setSelectedNode(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUnitLabel = (unit: DelayUnitEnum) => {
|
||||||
|
const units = {
|
||||||
|
[DelayUnitEnum.SECONDS]: "Seconds",
|
||||||
|
[DelayUnitEnum.MINUTES]: "Minutes",
|
||||||
|
[DelayUnitEnum.HOURS]: "Hours",
|
||||||
|
[DelayUnitEnum.DAYS]: "Days",
|
||||||
|
};
|
||||||
|
return units[unit] || unit;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTimeDescription = () => {
|
||||||
|
const value = delay.value || 0;
|
||||||
|
|
||||||
|
if (value <= 0) return "Invalid time";
|
||||||
|
|
||||||
|
if (value === 1) {
|
||||||
|
return `1 ${getUnitLabel(delay.unit).slice(0, -1)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${value} ${getUnitLabel(delay.unit)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="p-4 border-b border-neutral-700 flex-shrink-0">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-md font-medium text-neutral-200">Delay Duration</h3>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs bg-yellow-900/20 text-yellow-400 border-yellow-700/50"
|
||||||
|
>
|
||||||
|
{getTimeDescription().toUpperCase()}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={delay.unit}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setDelay({
|
||||||
|
...delay,
|
||||||
|
unit: value as DelayUnitEnum,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[120px] h-8 bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||||
|
<SelectValue placeholder="Unit" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||||
|
<SelectItem value={DelayUnitEnum.SECONDS}>Seconds</SelectItem>
|
||||||
|
<SelectItem value={DelayUnitEnum.MINUTES}>Minutes</SelectItem>
|
||||||
|
<SelectItem value={DelayUnitEnum.HOURS}>Hours</SelectItem>
|
||||||
|
<SelectItem value={DelayUnitEnum.DAYS}>Days</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto p-4 min-h-0">
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="p-3 rounded-md bg-yellow-900/10 border border-yellow-700/30 mb-2">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className="bg-yellow-900/50 rounded-full p-1.5 flex-shrink-0">
|
||||||
|
<Clock size={18} className="text-yellow-300" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="font-medium text-neutral-200">Time Delay</h3>
|
||||||
|
<p className="text-sm text-neutral-400 mt-1">
|
||||||
|
Pause workflow execution for a specified amount of time
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="delay-value">Delay Value</Label>
|
||||||
|
<Input
|
||||||
|
id="delay-value"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
className="bg-neutral-700 border-neutral-600"
|
||||||
|
value={delay.value}
|
||||||
|
onChange={(e) =>
|
||||||
|
setDelay({
|
||||||
|
...delay,
|
||||||
|
value: parseInt(e.target.value) || 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="delay-description">Description (optional)</Label>
|
||||||
|
<Textarea
|
||||||
|
id="delay-description"
|
||||||
|
className="bg-neutral-700 border-neutral-600 min-h-[100px] resize-none"
|
||||||
|
value={delay.description}
|
||||||
|
onChange={(e) =>
|
||||||
|
setDelay({
|
||||||
|
...delay,
|
||||||
|
description: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="Add a description for this delay"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{delay.value > 0 ? (
|
||||||
|
<div className="rounded-md bg-neutral-700/50 border border-neutral-600 p-3 mt-2">
|
||||||
|
<div className="text-sm font-medium text-neutral-400 mb-1">Preview</div>
|
||||||
|
<div className="flex items-start gap-2 p-2 rounded-md bg-neutral-800/70">
|
||||||
|
<div className="rounded-full bg-yellow-900/30 p-1.5 mt-0.5">
|
||||||
|
<HourglassIcon size={15} className="text-yellow-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm text-yellow-400 font-medium">
|
||||||
|
{getTimeDescription()} delay
|
||||||
|
</span>
|
||||||
|
{delay.description && (
|
||||||
|
<span className="text-xs text-neutral-400 mt-1">
|
||||||
|
{delay.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-md bg-neutral-700/30 border border-neutral-600/50 p-4 flex flex-col items-center justify-center text-center">
|
||||||
|
<AlertCircle className="h-6 w-6 text-neutral-500 mb-2" />
|
||||||
|
<p className="text-neutral-400 text-sm">Please set a valid delay time</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 border-t border-neutral-700 flex-shrink-0">
|
||||||
|
<div className="flex gap-2 justify-between">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="border-red-700/50 bg-red-900/20 text-red-400 hover:bg-red-900/30 hover:text-red-300"
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
Delete Node
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="bg-yellow-700 hover:bg-yellow-600 text-white flex items-center gap-2"
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
<Save size={16} />
|
||||||
|
Save Delay
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @author: Victor Calazans - Implementation of Delay node functionality │
|
||||||
|
│ @file: /app/agents/workflows/nodes/components/delay/DelayNode.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Delay node developed by: Victor Calazans │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Delay node implementation date: May 17, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { Handle, NodeProps, Position, useEdges } from "@xyflow/react";
|
||||||
|
import { Clock, ArrowRight, Timer } from "lucide-react";
|
||||||
|
import { DelayType } from "../../nodeFunctions";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import { BaseNode } from "../../BaseNode";
|
||||||
|
|
||||||
|
export function DelayNode(props: NodeProps) {
|
||||||
|
const { selected, data } = props;
|
||||||
|
|
||||||
|
const edges = useEdges();
|
||||||
|
const isExecuting = data.isExecuting as boolean | undefined;
|
||||||
|
|
||||||
|
const isHandleConnected = (handleId: string) => {
|
||||||
|
return edges.some(
|
||||||
|
(edge) => edge.source === props.id && edge.sourceHandle === handleId
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isBottomHandleConnected = isHandleConnected("bottom-handle");
|
||||||
|
|
||||||
|
const delay = data.delay as DelayType | undefined;
|
||||||
|
|
||||||
|
const getUnitLabel = (unit: string) => {
|
||||||
|
switch (unit) {
|
||||||
|
case 'seconds':
|
||||||
|
return 'Seconds';
|
||||||
|
case 'minutes':
|
||||||
|
return 'Minutes';
|
||||||
|
case 'hours':
|
||||||
|
return 'Hours';
|
||||||
|
case 'days':
|
||||||
|
return 'Days';
|
||||||
|
default:
|
||||||
|
return unit;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseNode hasTarget={true} selected={selected || false} borderColor="yellow" isExecuting={isExecuting}>
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-yellow-900/40 shadow-sm">
|
||||||
|
<Clock className="h-5 w-5 text-yellow-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-medium text-yellow-400">
|
||||||
|
{data.label as string}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{delay ? (
|
||||||
|
<div className="mb-3 rounded-lg border border-yellow-700/40 bg-yellow-950/10 p-3 transition-all duration-200 hover:border-yellow-600/50 hover:bg-yellow-900/10">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Timer className="h-4 w-4 text-yellow-400" />
|
||||||
|
<span className="ml-1.5 font-medium text-white">Delay</span>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="px-1.5 py-0 text-xs bg-yellow-900/30 text-yellow-400 border-yellow-700/40"
|
||||||
|
>
|
||||||
|
{getUnitLabel(delay.unit)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 flex items-center">
|
||||||
|
<span className="text-lg font-semibold text-yellow-300">{delay.value}</span>
|
||||||
|
<span className="ml-1 text-sm text-neutral-400">{delay.unit}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{delay.description && (
|
||||||
|
<p className="mt-2 text-xs text-neutral-400 line-clamp-2">
|
||||||
|
{delay.description.slice(0, 80)} {delay.description.length > 80 && '...'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mb-3 flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-yellow-700/40 bg-yellow-950/10 p-5 text-center transition-all duration-200 hover:border-yellow-600/50 hover:bg-yellow-900/20">
|
||||||
|
<Clock className="h-8 w-8 text-yellow-700/50 mb-2" />
|
||||||
|
<p className="text-yellow-400">No delay configured</p>
|
||||||
|
<p className="mt-1 text-xs text-neutral-500">Click to configure</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-2 flex items-center justify-end text-sm text-neutral-400 transition-colors">
|
||||||
|
<div className="flex items-center space-x-1 rounded-md py-1 px-2">
|
||||||
|
<span>Next step</span>
|
||||||
|
<ArrowRight className="h-3.5 w-3.5" />
|
||||||
|
</div>
|
||||||
|
<Handle
|
||||||
|
className={cn(
|
||||||
|
"!w-3 !h-3 !rounded-full transition-all duration-300",
|
||||||
|
isBottomHandleConnected ? "!bg-yellow-500 !border-yellow-400" : "!bg-neutral-400 !border-neutral-500",
|
||||||
|
selected && isBottomHandleConnected && "!bg-yellow-400 !border-yellow-300"
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
right: "-8px",
|
||||||
|
top: "calc(100% - 25px)",
|
||||||
|
}}
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
id="bottom-handle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</BaseNode>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/nodes/components/message/MessageForm.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
/* eslint-disable jsx-a11y/alt-text */
|
||||||
|
import { useEdges, useNodes } from "@xyflow/react";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { Agent } from "@/types/agent";
|
||||||
|
import { listAgents } from "@/services/agentService";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
MessageSquare,
|
||||||
|
Save,
|
||||||
|
Text,
|
||||||
|
Image,
|
||||||
|
File,
|
||||||
|
Video,
|
||||||
|
AlertCircle
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
function MessageForm({
|
||||||
|
selectedNode,
|
||||||
|
handleUpdateNode,
|
||||||
|
setEdges,
|
||||||
|
setIsOpen,
|
||||||
|
setSelectedNode,
|
||||||
|
}: {
|
||||||
|
selectedNode: any;
|
||||||
|
handleUpdateNode: any;
|
||||||
|
setEdges: any;
|
||||||
|
setIsOpen: any;
|
||||||
|
setSelectedNode: any;
|
||||||
|
}) {
|
||||||
|
const [node, setNode] = useState(selectedNode);
|
||||||
|
const [messageType, setMessageType] = useState("text");
|
||||||
|
const [content, setContent] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [agents, setAgents] = useState<Agent[]>([]);
|
||||||
|
const [allAgents, setAllAgents] = useState<Agent[]>([]);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const edges = useEdges();
|
||||||
|
const nodes = useNodes();
|
||||||
|
|
||||||
|
const connectedNode = useMemo(() => {
|
||||||
|
const edge = edges.find((edge) => edge.source === selectedNode.id);
|
||||||
|
if (!edge) return null;
|
||||||
|
const node = nodes.find((node) => node.id === edge.target);
|
||||||
|
return node || null;
|
||||||
|
}, [edges, nodes, selectedNode.id]);
|
||||||
|
|
||||||
|
const user = typeof window !== "undefined" ? JSON.parse(localStorage.getItem("user") || '{}') : {};
|
||||||
|
const clientId = user?.client_id || "";
|
||||||
|
|
||||||
|
const currentAgent = typeof window !== "undefined" ?
|
||||||
|
JSON.parse(localStorage.getItem("current_workflow_agent") || '{}') : {};
|
||||||
|
const currentAgentId = currentAgent?.id;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedNode) {
|
||||||
|
setNode(selectedNode);
|
||||||
|
setMessageType(selectedNode.data.message?.type || "text");
|
||||||
|
setContent(selectedNode.data.message?.content || "");
|
||||||
|
}
|
||||||
|
}, [selectedNode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!clientId) return;
|
||||||
|
setLoading(true);
|
||||||
|
listAgents(clientId)
|
||||||
|
.then((res) => {
|
||||||
|
const filteredAgents = res.data.filter((agent: Agent) => agent.id !== currentAgentId);
|
||||||
|
setAllAgents(filteredAgents);
|
||||||
|
setAgents(filteredAgents);
|
||||||
|
})
|
||||||
|
.catch((error) => console.error("Error loading agents:", error))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [clientId, currentAgentId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchTerm.trim() === "") {
|
||||||
|
setAgents(allAgents);
|
||||||
|
} else {
|
||||||
|
const filtered = allAgents.filter((agent) =>
|
||||||
|
agent.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
setAgents(filtered);
|
||||||
|
}
|
||||||
|
}, [searchTerm, allAgents]);
|
||||||
|
|
||||||
|
const handleDeleteEdge = useCallback(() => {
|
||||||
|
const id = edges.find((edge: any) => edge.source === selectedNode.id)?.id;
|
||||||
|
setEdges((edges: any) => {
|
||||||
|
const left = edges.filter((edge: any) => edge.id !== id);
|
||||||
|
return left;
|
||||||
|
});
|
||||||
|
}, [nodes, edges, selectedNode, setEdges]);
|
||||||
|
|
||||||
|
const handleSelectAgent = (agent: Agent) => {
|
||||||
|
const updatedNode = {
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
agent,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setNode(updatedNode);
|
||||||
|
handleUpdateNode(updatedNode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAgentTypeName = (type: string) => {
|
||||||
|
const agentTypes: Record<string, string> = {
|
||||||
|
llm: "LLM Agent",
|
||||||
|
a2a: "A2A Agent",
|
||||||
|
sequential: "Sequential Agent",
|
||||||
|
parallel: "Parallel Agent",
|
||||||
|
loop: "Loop Agent",
|
||||||
|
workflow: "Workflow Agent",
|
||||||
|
task: "Task Agent",
|
||||||
|
};
|
||||||
|
return agentTypes[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const updatedNode = {
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
message: {
|
||||||
|
type: messageType,
|
||||||
|
content,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setNode(updatedNode);
|
||||||
|
handleUpdateNode(updatedNode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const messageTypeInfo = {
|
||||||
|
text: {
|
||||||
|
icon: <Text className="h-5 w-5 text-orange-400" />,
|
||||||
|
name: "Text Message",
|
||||||
|
description: "Simple text message",
|
||||||
|
color: "bg-orange-900/30 border-orange-700/50",
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
icon: <Image className="h-5 w-5 text-blue-400" />,
|
||||||
|
name: "Image",
|
||||||
|
description: "Image URL or base64",
|
||||||
|
color: "bg-blue-900/30 border-blue-700/50",
|
||||||
|
},
|
||||||
|
file: {
|
||||||
|
icon: <File className="h-5 w-5 text-emerald-400" />,
|
||||||
|
name: "File",
|
||||||
|
description: "File URL or base64",
|
||||||
|
color: "bg-emerald-900/30 border-emerald-700/50",
|
||||||
|
},
|
||||||
|
video: {
|
||||||
|
icon: <Video className="h-5 w-5 text-purple-400" />,
|
||||||
|
name: "Video",
|
||||||
|
description: "Video URL or base64",
|
||||||
|
color: "bg-purple-900/30 border-purple-700/50",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="p-4 border-b border-neutral-700 flex-shrink-0">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-md font-medium text-neutral-200">Message Type</h3>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs bg-orange-900/20 text-orange-400 border-orange-700/50"
|
||||||
|
>
|
||||||
|
{messageType === "text" ? "TEXT" : messageType.toUpperCase()}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={messageType}
|
||||||
|
onValueChange={setMessageType}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[120px] h-8 bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||||
|
<SelectValue placeholder="Select type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||||
|
<SelectItem value="text">Text</SelectItem>
|
||||||
|
{/* Other options can be enabled in the future */}
|
||||||
|
{/* <SelectItem value="image">Image</SelectItem>
|
||||||
|
<SelectItem value="file">File</SelectItem>
|
||||||
|
<SelectItem value="video">Video</SelectItem> */}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto p-4 min-h-0">
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="p-3 rounded-md bg-orange-900/10 border border-orange-700/30 mb-2">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className="bg-orange-900/50 rounded-full p-1.5 flex-shrink-0">
|
||||||
|
<MessageSquare size={18} className="text-orange-300" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="font-medium text-neutral-200">{messageTypeInfo.text.name}</h3>
|
||||||
|
<p className="text-sm text-neutral-400 mt-1">{messageTypeInfo.text.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="content">Message Content</Label>
|
||||||
|
<Textarea
|
||||||
|
id="content"
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
placeholder="Type your message here..."
|
||||||
|
className="min-h-[150px] bg-neutral-700 border-neutral-600 resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{content.trim() !== "" ? (
|
||||||
|
<div className="rounded-md bg-neutral-700/50 border border-neutral-600 p-3 mt-2">
|
||||||
|
<div className="text-sm font-medium text-neutral-400 mb-1">Preview</div>
|
||||||
|
<div className="flex items-start gap-2 p-2 rounded-md bg-neutral-800/70">
|
||||||
|
<div className="rounded-full bg-orange-900/30 p-1.5 mt-0.5">
|
||||||
|
<MessageSquare size={15} className="text-orange-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-neutral-300 whitespace-pre-wrap">{content}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-md bg-neutral-700/30 border border-neutral-600/50 p-4 flex flex-col items-center justify-center text-center">
|
||||||
|
<AlertCircle className="h-6 w-6 text-neutral-500 mb-2" />
|
||||||
|
<p className="text-neutral-400 text-sm">Your message will appear here</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 border-t border-neutral-700 flex-shrink-0">
|
||||||
|
<Button
|
||||||
|
className="w-full bg-orange-700 hover:bg-orange-600 text-white flex items-center gap-2 justify-center"
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
<Save size={16} />
|
||||||
|
Save Message
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { MessageForm };
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/nodes/components/message/MessageNode.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { Handle, NodeProps, Position, useEdges } from "@xyflow/react";
|
||||||
|
import { MessageCircle, Text, Image, File, Video, ArrowRight } from "lucide-react";
|
||||||
|
import { MessageType, MessageTypeEnum } from "../../nodeFunctions";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import { BaseNode } from "../../BaseNode";
|
||||||
|
|
||||||
|
export function MessageNode(props: NodeProps) {
|
||||||
|
const { selected, data } = props;
|
||||||
|
const edges = useEdges();
|
||||||
|
const isExecuting = data.isExecuting as boolean | undefined;
|
||||||
|
|
||||||
|
const isHandleConnected = (handleId: string) => {
|
||||||
|
return edges.some(
|
||||||
|
(edge) => edge.source === props.id && edge.sourceHandle === handleId
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isBottomHandleConnected = isHandleConnected("bottom-handle");
|
||||||
|
|
||||||
|
const message = data.message as MessageType | undefined;
|
||||||
|
|
||||||
|
const getMessageTypeIcon = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case MessageTypeEnum.TEXT:
|
||||||
|
return <Text className="h-4 w-4 text-orange-400" />;
|
||||||
|
case "image":
|
||||||
|
return <Image className="h-4 w-4 text-blue-400" />;
|
||||||
|
case "file":
|
||||||
|
return <File className="h-4 w-4 text-emerald-400" />;
|
||||||
|
case "video":
|
||||||
|
return <Video className="h-4 w-4 text-purple-400" />;
|
||||||
|
default:
|
||||||
|
return <MessageCircle className="h-4 w-4 text-orange-400" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMessageTypeColor = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case MessageTypeEnum.TEXT:
|
||||||
|
return 'bg-orange-900/30 text-orange-400 border-orange-700/40';
|
||||||
|
case "image":
|
||||||
|
return 'bg-blue-900/30 text-blue-400 border-blue-700/40';
|
||||||
|
case "file":
|
||||||
|
return 'bg-emerald-900/30 text-emerald-400 border-emerald-700/40';
|
||||||
|
case "video":
|
||||||
|
return 'bg-purple-900/30 text-purple-400 border-purple-700/40';
|
||||||
|
default:
|
||||||
|
return 'bg-orange-900/30 text-orange-400 border-orange-700/40';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMessageTypeName = (type: string) => {
|
||||||
|
const messageTypes: Record<string, string> = {
|
||||||
|
text: "Text Message",
|
||||||
|
image: "Image",
|
||||||
|
file: "File",
|
||||||
|
video: "Video",
|
||||||
|
};
|
||||||
|
return messageTypes[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseNode hasTarget={true} selected={selected || false} borderColor="orange" isExecuting={isExecuting}>
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-orange-900/40 shadow-sm">
|
||||||
|
<MessageCircle className="h-5 w-5 text-orange-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-medium text-orange-400">
|
||||||
|
{data.label as string}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{message ? (
|
||||||
|
<div className="mb-3 rounded-lg border border-orange-700/40 bg-orange-950/10 p-3 transition-all duration-200 hover:border-orange-600/50 hover:bg-orange-900/10">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{getMessageTypeIcon(message.type)}
|
||||||
|
<span className="ml-1.5 font-medium text-white">{getMessageTypeName(message.type)}</span>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={cn("px-1.5 py-0 text-xs", getMessageTypeColor(message.type))}
|
||||||
|
>
|
||||||
|
{message.type}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{message.content && (
|
||||||
|
<p className="mt-2 text-xs text-neutral-400 line-clamp-2">
|
||||||
|
{message.content.slice(0, 80)} {message.content.length > 80 && '...'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mb-3 flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-orange-700/40 bg-orange-950/10 p-5 text-center transition-all duration-200 hover:border-orange-600/50 hover:bg-orange-900/20">
|
||||||
|
<MessageCircle className="h-8 w-8 text-orange-700/50 mb-2" />
|
||||||
|
<p className="text-orange-400">No message configured</p>
|
||||||
|
<p className="mt-1 text-xs text-neutral-500">Click to configure</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-2 flex items-center justify-end text-sm text-neutral-400 transition-colors">
|
||||||
|
<div className="flex items-center space-x-1 rounded-md py-1 px-2">
|
||||||
|
<span>Next step</span>
|
||||||
|
<ArrowRight className="h-3.5 w-3.5" />
|
||||||
|
</div>
|
||||||
|
<Handle
|
||||||
|
className={cn(
|
||||||
|
"!w-3 !h-3 !rounded-full transition-all duration-300",
|
||||||
|
isBottomHandleConnected ? "!bg-orange-500 !border-orange-400" : "!bg-neutral-400 !border-neutral-500",
|
||||||
|
selected && isBottomHandleConnected && "!bg-orange-400 !border-orange-300"
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
right: "-8px",
|
||||||
|
top: "calc(100% - 25px)",
|
||||||
|
}}
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
id="bottom-handle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</BaseNode>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/nodes/components/start/StartNode.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { Handle, Node, NodeProps, Position, useEdges } from "@xyflow/react";
|
||||||
|
import { Zap, ArrowRight } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import { BaseNode } from "../../BaseNode";
|
||||||
|
|
||||||
|
export type StartNodeType = Node<
|
||||||
|
{
|
||||||
|
label?: string;
|
||||||
|
},
|
||||||
|
"start-node"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function StartNode(props: NodeProps) {
|
||||||
|
const { selected, data } = props;
|
||||||
|
const edges = useEdges();
|
||||||
|
const isExecuting = data.isExecuting as boolean | undefined;
|
||||||
|
|
||||||
|
const isSourceHandleConnected = edges.some(
|
||||||
|
(edge) => edge.source === props.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseNode hasTarget={true} selected={selected || false} borderColor="emerald" isExecuting={isExecuting}>
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-emerald-900/40 shadow-sm">
|
||||||
|
<Zap className="h-5 w-5 text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-medium text-emerald-400">Start</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3 rounded-lg border border-emerald-700/40 bg-emerald-950/10 p-3 transition-all duration-200 hover:border-emerald-600/50 hover:bg-emerald-900/10">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="font-medium text-white">Input: User content</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-2 text-xs text-neutral-400">
|
||||||
|
The workflow begins when a user sends a message to the agent
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 flex items-center justify-end text-sm text-neutral-400 transition-colors">
|
||||||
|
<div className="flex items-center space-x-1 rounded-md py-1 px-2">
|
||||||
|
<span>Next step</span>
|
||||||
|
<ArrowRight className="h-3.5 w-3.5" />
|
||||||
|
</div>
|
||||||
|
<Handle
|
||||||
|
className={cn(
|
||||||
|
"!w-3 !h-3 !rounded-full transition-all duration-300",
|
||||||
|
isSourceHandleConnected ? "!bg-emerald-500 !border-emerald-400" : "!bg-neutral-400 !border-neutral-500",
|
||||||
|
selected && isSourceHandleConnected && "!bg-emerald-400 !border-emerald-300"
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
right: "-8px",
|
||||||
|
top: "calc(100% - 25px)",
|
||||||
|
}}
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</BaseNode>
|
||||||
|
);
|
||||||
|
}
|
||||||
109
frontend/app/agents/workflows/nodes/index.ts
Normal file
109
frontend/app/agents/workflows/nodes/index.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @author: Victor Calazans - Implementation of Delay node type │
|
||||||
|
│ @file: /app/agents/workflows/nodes/index.ts │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Delay node functionality developed by: Victor Calazans │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Delay implementation date: May 17, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
import type { NodeTypes, BuiltInNode, Node } from "@xyflow/react";
|
||||||
|
|
||||||
|
import { ConditionNode } from "./components/condition/ConditionNode";
|
||||||
|
import { AgentNode } from "./components/agent/AgentNode";
|
||||||
|
import { StartNode, StartNodeType } from "./components/start/StartNode";
|
||||||
|
import { MessageNode } from "./components/message/MessageNode";
|
||||||
|
import { DelayNode } from "./components/delay/DelayNode";
|
||||||
|
|
||||||
|
import "./style.css";
|
||||||
|
import {
|
||||||
|
ConditionType,
|
||||||
|
MessageType,
|
||||||
|
DelayType,
|
||||||
|
} from "./nodeFunctions";
|
||||||
|
import { Agent } from "@/types/agent";
|
||||||
|
|
||||||
|
type AgentNodeType = Node<
|
||||||
|
{
|
||||||
|
label?: string;
|
||||||
|
agent?: Agent;
|
||||||
|
},
|
||||||
|
"agent-node"
|
||||||
|
>;
|
||||||
|
|
||||||
|
type MessageNodeType = Node<
|
||||||
|
{
|
||||||
|
label?: string;
|
||||||
|
message?: MessageType;
|
||||||
|
},
|
||||||
|
"message-node"
|
||||||
|
>;
|
||||||
|
|
||||||
|
type DelayNodeType = Node<
|
||||||
|
{
|
||||||
|
label?: string;
|
||||||
|
delay?: DelayType;
|
||||||
|
},
|
||||||
|
"delay-node"
|
||||||
|
>;
|
||||||
|
|
||||||
|
type ConditionNodeType = Node<
|
||||||
|
{
|
||||||
|
label?: string;
|
||||||
|
integration?: string;
|
||||||
|
icon?: string;
|
||||||
|
conditions?: ConditionType[];
|
||||||
|
},
|
||||||
|
"condition-node"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type AppNode =
|
||||||
|
| BuiltInNode
|
||||||
|
| StartNodeType
|
||||||
|
| AgentNodeType
|
||||||
|
| ConditionNodeType
|
||||||
|
| MessageNodeType
|
||||||
|
| DelayNodeType;
|
||||||
|
|
||||||
|
export type NodeType = AppNode["type"];
|
||||||
|
|
||||||
|
export const initialNodes: AppNode[] = [
|
||||||
|
{
|
||||||
|
id: "start-node",
|
||||||
|
type: "start-node",
|
||||||
|
position: { x: -100, y: 100 },
|
||||||
|
data: {
|
||||||
|
label: "Start",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const nodeTypes = {
|
||||||
|
"start-node": StartNode,
|
||||||
|
"agent-node": AgentNode,
|
||||||
|
"message-node": MessageNode,
|
||||||
|
"condition-node": ConditionNode,
|
||||||
|
"delay-node": DelayNode,
|
||||||
|
} satisfies NodeTypes;
|
||||||
65
frontend/app/agents/workflows/nodes/nodeFunctions.ts
Normal file
65
frontend/app/agents/workflows/nodes/nodeFunctions.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @author: Victor Calazans - Implementation of Delay types │
|
||||||
|
│ @file: /app/agents/workflows/nodes/nodeFunctions.ts │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Delay node functionality developed by: Victor Calazans │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Delay implementation date: May 17, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
export enum ConditionTypeEnum {
|
||||||
|
PREVIOUS_OUTPUT = "previous-output",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MessageTypeEnum {
|
||||||
|
TEXT = "text",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DelayUnitEnum {
|
||||||
|
SECONDS = "seconds",
|
||||||
|
MINUTES = "minutes",
|
||||||
|
HOURS = "hours",
|
||||||
|
DAYS = "days",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MessageType = {
|
||||||
|
type: MessageTypeEnum;
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DelayType = {
|
||||||
|
value: number;
|
||||||
|
unit: DelayUnitEnum;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConditionType = {
|
||||||
|
id: string;
|
||||||
|
type: ConditionTypeEnum;
|
||||||
|
data?: any;
|
||||||
|
};
|
||||||
|
|
||||||
54
frontend/app/agents/workflows/nodes/style.css
Normal file
54
frontend/app/agents/workflows/nodes/style.css
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/nodes/style.css │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
/* .react-flow__handle {
|
||||||
|
background-color: #8492A6;
|
||||||
|
border-radius: 50%;
|
||||||
|
height: 16px;
|
||||||
|
position: absolute;
|
||||||
|
width: 16px;
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* .react-flow__handle-right {
|
||||||
|
right: -6px;
|
||||||
|
top: 88%;
|
||||||
|
transform: translateY(-75%);
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
border: 3px solid #8492A6 !important;
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* .react-flow__handle-left {
|
||||||
|
left: 0px;
|
||||||
|
top: 40%;
|
||||||
|
width: 60px;
|
||||||
|
border-radius: 0;
|
||||||
|
height: 50%;
|
||||||
|
transform: translateY(-75%);
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
} */
|
||||||
218
frontend/app/agents/workflows/page.tsx
Normal file
218
frontend/app/agents/workflows/page.tsx
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/page.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState, useRef, Suspense } from "react";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import Canva from "./Canva";
|
||||||
|
import { Agent } from "@/types/agent";
|
||||||
|
import { getAgent, updateAgent } from "@/services/agentService";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ArrowLeft, Save, Download, PlayIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { ReactFlowProvider } from "@xyflow/react";
|
||||||
|
import { DnDProvider } from "@/contexts/DnDContext";
|
||||||
|
import { NodeDataProvider } from "@/contexts/NodeDataContext";
|
||||||
|
import { SourceClickProvider } from "@/contexts/SourceClickContext";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { AgentTestChatModal } from "./nodes/components/agent/AgentTestChatModal";
|
||||||
|
|
||||||
|
function WorkflowsContent() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const agentId = searchParams.get("agentId");
|
||||||
|
const [agent, setAgent] = useState<Agent | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const canvaRef = useRef<any>(null);
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [isTestModalOpen, setIsTestModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const user =
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? JSON.parse(localStorage.getItem("user") || "{}")
|
||||||
|
: {};
|
||||||
|
const clientId = user?.client_id || "";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (agentId && clientId) {
|
||||||
|
setLoading(true);
|
||||||
|
getAgent(agentId, clientId)
|
||||||
|
.then((res) => {
|
||||||
|
setAgent(res.data);
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
localStorage.setItem(
|
||||||
|
"current_workflow_agent",
|
||||||
|
JSON.stringify(res.data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Error loading agent:", err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [agentId, clientId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
localStorage.removeItem("current_workflow_agent");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSaveWorkflow = async () => {
|
||||||
|
if (!agent || !canvaRef.current) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { nodes, edges } = canvaRef.current.getFlowData();
|
||||||
|
|
||||||
|
const workflow = {
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateAgent(agent.id, {
|
||||||
|
...agent,
|
||||||
|
config: {
|
||||||
|
...agent.config,
|
||||||
|
workflow,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Workflow saved",
|
||||||
|
description: "The changes were saved successfully",
|
||||||
|
});
|
||||||
|
|
||||||
|
canvaRef.current.setHasChanges(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error saving workflow:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error saving workflow",
|
||||||
|
description: "Unable to save the changes",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-screen bg-[#121212] flex items-center justify-center">
|
||||||
|
<div className="text-white text-xl">Loading...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full h-screen flex flex-col" data-workflow-page="true">
|
||||||
|
{/* Header with controls */}
|
||||||
|
<div className="w-full bg-[#121212] py-4 px-6 z-10 flex items-center justify-between border-b border-neutral-800">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Link href="/agents">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="bg-neutral-800 border-neutral-700 text-neutral-200 hover:bg-neutral-700"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Back to Agents
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{agent && (
|
||||||
|
<div className="bg-neutral-800 px-4 py-2 rounded-md">
|
||||||
|
<h2 className="text-neutral-200 font-medium">
|
||||||
|
{agent.name} -{" "}
|
||||||
|
<span className="text-neutral-400 text-sm">{agent.type}</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="bg-neutral-800 border-neutral-700 text-neutral-200 hover:bg-neutral-700"
|
||||||
|
onClick={handleSaveWorkflow}
|
||||||
|
>
|
||||||
|
<Save className="mr-2 h-4 w-4" />
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
{agent && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="bg-green-800 border-green-700 text-green-200 hover:bg-green-700"
|
||||||
|
onClick={() => setIsTestModalOpen(true)}
|
||||||
|
>
|
||||||
|
<PlayIcon className="h-4 w-4 mr-2" />
|
||||||
|
Test Workflow
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content area */}
|
||||||
|
<div className="flex-1 relative overflow-hidden">
|
||||||
|
{agent && isTestModalOpen && (
|
||||||
|
<AgentTestChatModal
|
||||||
|
open={isTestModalOpen}
|
||||||
|
onOpenChange={setIsTestModalOpen}
|
||||||
|
agent={agent}
|
||||||
|
canvasRef={canvaRef} // Pass the canvas reference to allow visualization of running nodes
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<NodeDataProvider>
|
||||||
|
<SourceClickProvider>
|
||||||
|
<DnDProvider>
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<Canva agent={agent} ref={canvaRef} data-canvas-ref="true" />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
</DnDProvider>
|
||||||
|
</SourceClickProvider>
|
||||||
|
</NodeDataProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WorkflowsPage() {
|
||||||
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="w-full h-screen bg-[#121212] flex items-center justify-center">
|
||||||
|
<div className="text-white text-xl">Loading...</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<WorkflowsContent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
199
frontend/app/agents/workflows/utils.ts
Normal file
199
frontend/app/agents/workflows/utils.ts
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/agents/workflows/utils.ts │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
import { Node, NodePositionChange, XYPosition } from "@xyflow/react";
|
||||||
|
|
||||||
|
type GetHelperLinesResult = {
|
||||||
|
horizontal?: number;
|
||||||
|
vertical?: number;
|
||||||
|
snapPosition: Partial<XYPosition>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// this utility function can be called with a position change (inside onNodesChange)
|
||||||
|
// it checks all other nodes and calculated the helper line positions and the position where the current node should snap to
|
||||||
|
export function getHelperLines(
|
||||||
|
change: NodePositionChange,
|
||||||
|
nodes: Node[],
|
||||||
|
distance = 5,
|
||||||
|
): GetHelperLinesResult {
|
||||||
|
const defaultResult = {
|
||||||
|
horizontal: undefined,
|
||||||
|
vertical: undefined,
|
||||||
|
snapPosition: { x: undefined, y: undefined },
|
||||||
|
};
|
||||||
|
const nodeA = nodes.find((node) => node.id === change.id);
|
||||||
|
|
||||||
|
if (!nodeA || !change.position) {
|
||||||
|
return defaultResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeABounds = {
|
||||||
|
left: change.position.x,
|
||||||
|
right: change.position.x + (nodeA.measured?.width ?? 0),
|
||||||
|
top: change.position.y,
|
||||||
|
bottom: change.position.y + (nodeA.measured?.height ?? 0),
|
||||||
|
width: nodeA.measured?.width ?? 0,
|
||||||
|
height: nodeA.measured?.height ?? 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let horizontalDistance = distance;
|
||||||
|
let verticalDistance = distance;
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
.filter((node) => node.id !== nodeA.id)
|
||||||
|
.reduce<GetHelperLinesResult>((result, nodeB) => {
|
||||||
|
const nodeBBounds = {
|
||||||
|
left: nodeB.position.x,
|
||||||
|
right: nodeB.position.x + (nodeB.measured?.width ?? 0),
|
||||||
|
top: nodeB.position.y,
|
||||||
|
bottom: nodeB.position.y + (nodeB.measured?.height ?? 0),
|
||||||
|
width: nodeB.measured?.width ?? 0,
|
||||||
|
height: nodeB.measured?.height ?? 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||||
|
// | A |
|
||||||
|
// |___________|
|
||||||
|
// |
|
||||||
|
// |
|
||||||
|
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||||
|
// | B |
|
||||||
|
// |___________|
|
||||||
|
const distanceLeftLeft = Math.abs(nodeABounds.left - nodeBBounds.left);
|
||||||
|
|
||||||
|
if (distanceLeftLeft < verticalDistance) {
|
||||||
|
result.snapPosition.x = nodeBBounds.left;
|
||||||
|
result.vertical = nodeBBounds.left;
|
||||||
|
verticalDistance = distanceLeftLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||||
|
// | A |
|
||||||
|
// |___________|
|
||||||
|
// |
|
||||||
|
// |
|
||||||
|
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||||
|
// | B |
|
||||||
|
// |___________|
|
||||||
|
const distanceRightRight = Math.abs(
|
||||||
|
nodeABounds.right - nodeBBounds.right,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distanceRightRight < verticalDistance) {
|
||||||
|
result.snapPosition.x = nodeBBounds.right - nodeABounds.width;
|
||||||
|
result.vertical = nodeBBounds.right;
|
||||||
|
verticalDistance = distanceRightRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||||
|
// | A |
|
||||||
|
// |___________|
|
||||||
|
// |
|
||||||
|
// |
|
||||||
|
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||||
|
// | B |
|
||||||
|
// |___________|
|
||||||
|
const distanceLeftRight = Math.abs(nodeABounds.left - nodeBBounds.right);
|
||||||
|
|
||||||
|
if (distanceLeftRight < verticalDistance) {
|
||||||
|
result.snapPosition.x = nodeBBounds.right;
|
||||||
|
result.vertical = nodeBBounds.right;
|
||||||
|
verticalDistance = distanceLeftRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||||
|
// | A |
|
||||||
|
// |___________|
|
||||||
|
// |
|
||||||
|
// |
|
||||||
|
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||||
|
// | B |
|
||||||
|
// |___________|
|
||||||
|
const distanceRightLeft = Math.abs(nodeABounds.right - nodeBBounds.left);
|
||||||
|
|
||||||
|
if (distanceRightLeft < verticalDistance) {
|
||||||
|
result.snapPosition.x = nodeBBounds.left - nodeABounds.width;
|
||||||
|
result.vertical = nodeBBounds.left;
|
||||||
|
verticalDistance = distanceRightLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
// |‾‾‾‾‾‾‾‾‾‾‾|‾‾‾‾‾|‾‾‾‾‾‾‾‾‾‾‾|
|
||||||
|
// | A | | B |
|
||||||
|
// |___________| |___________|
|
||||||
|
const distanceTopTop = Math.abs(nodeABounds.top - nodeBBounds.top);
|
||||||
|
|
||||||
|
if (distanceTopTop < horizontalDistance) {
|
||||||
|
result.snapPosition.y = nodeBBounds.top;
|
||||||
|
result.horizontal = nodeBBounds.top;
|
||||||
|
horizontalDistance = distanceTopTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||||
|
// | A |
|
||||||
|
// |___________|_________________
|
||||||
|
// | |
|
||||||
|
// | B |
|
||||||
|
// |___________|
|
||||||
|
const distanceBottomTop = Math.abs(nodeABounds.bottom - nodeBBounds.top);
|
||||||
|
|
||||||
|
if (distanceBottomTop < horizontalDistance) {
|
||||||
|
result.snapPosition.y = nodeBBounds.top - nodeABounds.height;
|
||||||
|
result.horizontal = nodeBBounds.top;
|
||||||
|
horizontalDistance = distanceBottomTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// |‾‾‾‾‾‾‾‾‾‾‾| |‾‾‾‾‾‾‾‾‾‾‾|
|
||||||
|
// | A | | B |
|
||||||
|
// |___________|_____|___________|
|
||||||
|
const distanceBottomBottom = Math.abs(
|
||||||
|
nodeABounds.bottom - nodeBBounds.bottom,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distanceBottomBottom < horizontalDistance) {
|
||||||
|
result.snapPosition.y = nodeBBounds.bottom - nodeABounds.height;
|
||||||
|
result.horizontal = nodeBBounds.bottom;
|
||||||
|
horizontalDistance = distanceBottomBottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||||
|
// | B |
|
||||||
|
// | |
|
||||||
|
// |‾‾‾‾‾‾‾‾‾‾‾|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
||||||
|
// | A |
|
||||||
|
// |___________|
|
||||||
|
const distanceTopBottom = Math.abs(nodeABounds.top - nodeBBounds.bottom);
|
||||||
|
|
||||||
|
if (distanceTopBottom < horizontalDistance) {
|
||||||
|
result.snapPosition.y = nodeBBounds.bottom;
|
||||||
|
result.horizontal = nodeBBounds.bottom;
|
||||||
|
horizontalDistance = distanceTopBottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, defaultResult);
|
||||||
|
}
|
||||||
497
frontend/app/chat/components/AgentInfoDialog.tsx
Normal file
497
frontend/app/chat/components/AgentInfoDialog.tsx
Normal file
@@ -0,0 +1,497 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/chat/components/AgentInfoDialog.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 14, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Agent } from "@/types/agent";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Bot,
|
||||||
|
Code,
|
||||||
|
WrenchIcon,
|
||||||
|
Layers,
|
||||||
|
Server,
|
||||||
|
TagIcon,
|
||||||
|
Share,
|
||||||
|
Edit,
|
||||||
|
Loader2,
|
||||||
|
Download,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { listApiKeys, ApiKey } from "@/services/agentService";
|
||||||
|
import { listMCPServers } from "@/services/mcpServerService";
|
||||||
|
import { availableModels } from "@/types/aiModels";
|
||||||
|
import { MCPServer } from "@/types/mcpServer";
|
||||||
|
import { AgentForm } from "@/app/agents/forms/AgentForm";
|
||||||
|
import { exportAsJson } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface AgentInfoDialogProps {
|
||||||
|
agent: Agent | null;
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onAgentUpdated?: (updatedAgent: Agent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgentInfoDialog({
|
||||||
|
agent,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onAgentUpdated,
|
||||||
|
}: AgentInfoDialogProps) {
|
||||||
|
const [activeTab, setActiveTab] = useState("info");
|
||||||
|
const [isAgentFormOpen, setIsAgentFormOpen] = useState(false);
|
||||||
|
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
|
||||||
|
const [availableMCPs, setAvailableMCPs] = useState<MCPServer[]>([]);
|
||||||
|
const [agents, setAgents] = useState<Agent[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const user =
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? JSON.parse(localStorage.getItem("user") || "{}")
|
||||||
|
: {};
|
||||||
|
const clientId = user?.client_id || "";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!clientId || !open) return;
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
try {
|
||||||
|
const [apiKeysResponse, mcpServersResponse] = await Promise.all([
|
||||||
|
listApiKeys(clientId),
|
||||||
|
listMCPServers(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setApiKeys(apiKeysResponse.data);
|
||||||
|
setAvailableMCPs(mcpServersResponse.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading data for agent form:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
}, [clientId, open]);
|
||||||
|
|
||||||
|
const getAgentTypeName = (type: string) => {
|
||||||
|
const agentTypes: Record<string, string> = {
|
||||||
|
llm: "LLM Agent",
|
||||||
|
a2a: "A2A Agent",
|
||||||
|
sequential: "Sequential Agent",
|
||||||
|
parallel: "Parallel Agent",
|
||||||
|
loop: "Loop Agent",
|
||||||
|
workflow: "Workflow Agent",
|
||||||
|
task: "Task Agent",
|
||||||
|
};
|
||||||
|
return agentTypes[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveAgent = async (agentData: Partial<Agent>) => {
|
||||||
|
if (!agent?.id) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const { updateAgent } = await import("@/services/agentService");
|
||||||
|
const updated = await updateAgent(agent.id, agentData as any);
|
||||||
|
|
||||||
|
if (updated.data && onAgentUpdated) {
|
||||||
|
onAgentUpdated(updated.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAgentFormOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating agent:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to export the agent as JSON
|
||||||
|
const handleExportAgent = async () => {
|
||||||
|
if (!agent) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First fetch all agents to properly resolve agent_tools references
|
||||||
|
const { listAgents } = await import("@/services/agentService");
|
||||||
|
const allAgentsResponse = await listAgents(clientId, 0, 1000);
|
||||||
|
const allAgents = allAgentsResponse.data || [];
|
||||||
|
|
||||||
|
exportAsJson(
|
||||||
|
agent,
|
||||||
|
`agent-${agent.name.replace(/\s+/g, "-").toLowerCase()}-${agent.id.substring(0, 8)}`,
|
||||||
|
true,
|
||||||
|
allAgents
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error exporting agent:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!agent) return null;
|
||||||
|
|
||||||
|
const getToolsCount = () => {
|
||||||
|
let count = 0;
|
||||||
|
if (agent.config?.tools) count += agent.config.tools.length;
|
||||||
|
if (agent.config?.custom_tools?.http_tools)
|
||||||
|
count += agent.config.custom_tools.http_tools.length;
|
||||||
|
if (agent.config?.agent_tools)
|
||||||
|
count += agent.config.agent_tools.length;
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSubAgentsCount = () => {
|
||||||
|
return agent.config?.sub_agents?.length || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMCPServersCount = () => {
|
||||||
|
let count = 0;
|
||||||
|
if (agent.config?.mcp_servers) count += agent.config.mcp_servers.length;
|
||||||
|
if (agent.config?.custom_mcp_servers)
|
||||||
|
count += agent.config.custom_mcp_servers.length;
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-[600px] max-h-[85vh] flex flex-col overflow-hidden bg-neutral-900 border-neutral-700">
|
||||||
|
<DialogHeader className="flex flex-row items-start justify-between pb-2">
|
||||||
|
<div>
|
||||||
|
<DialogTitle className="text-xl text-white flex items-center gap-2">
|
||||||
|
<Bot className="h-5 w-5 text-emerald-400" />
|
||||||
|
{agent.name}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-neutral-400 mt-1">
|
||||||
|
{agent.description || "No description available"}
|
||||||
|
</DialogDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="bg-neutral-800 border-neutral-700 text-emerald-400"
|
||||||
|
>
|
||||||
|
{getAgentTypeName(agent.type)}
|
||||||
|
</Badge>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 rounded-full bg-neutral-800 border-neutral-700 hover:bg-emerald-900 hover:text-emerald-400"
|
||||||
|
onClick={handleExportAgent}
|
||||||
|
title="Export agent as JSON"
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 rounded-full bg-neutral-800 border-neutral-700 hover:bg-emerald-900 hover:text-emerald-400"
|
||||||
|
onClick={() => setIsAgentFormOpen(true)}
|
||||||
|
title="Edit agent"
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onValueChange={setActiveTab}
|
||||||
|
className="flex-1 overflow-hidden flex flex-col"
|
||||||
|
>
|
||||||
|
<TabsList className="bg-neutral-800 p-1 border-b border-neutral-700">
|
||||||
|
<TabsTrigger
|
||||||
|
value="info"
|
||||||
|
className="data-[state=active]:bg-neutral-700 data-[state=active]:text-emerald-400"
|
||||||
|
>
|
||||||
|
Information
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="tools"
|
||||||
|
className="data-[state=active]:bg-neutral-700 data-[state=active]:text-emerald-400"
|
||||||
|
>
|
||||||
|
Tools
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="config"
|
||||||
|
className="data-[state=active]:bg-neutral-700 data-[state=active]:text-emerald-400"
|
||||||
|
>
|
||||||
|
Configuration
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<ScrollArea className="flex-1 p-4">
|
||||||
|
<TabsContent value="info" className="mt-0 space-y-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<div className="bg-neutral-800 p-3 rounded-md border border-neutral-700 flex flex-col items-center justify-center text-center">
|
||||||
|
<Code className="h-5 w-5 text-emerald-400 mb-1" />
|
||||||
|
<span className="text-xs text-neutral-400">Model</span>
|
||||||
|
<span className="text-sm text-neutral-200 mt-1 font-medium">
|
||||||
|
{agent.model || "Not specified"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-neutral-800 p-3 rounded-md border border-neutral-700 flex flex-col items-center justify-center text-center">
|
||||||
|
<TagIcon className="h-5 w-5 text-emerald-400 mb-1" />
|
||||||
|
<span className="text-xs text-neutral-400">Tools</span>
|
||||||
|
<span className="text-sm text-neutral-200 mt-1 font-medium">
|
||||||
|
{getToolsCount()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-neutral-800 p-3 rounded-md border border-neutral-700 flex flex-col items-center justify-center text-center">
|
||||||
|
<Layers className="h-5 w-5 text-emerald-400 mb-1" />
|
||||||
|
<span className="text-xs text-neutral-400">
|
||||||
|
Sub-agents
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-neutral-200 mt-1 font-medium">
|
||||||
|
{getSubAgentsCount()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-neutral-800 p-4 rounded-md border border-neutral-700">
|
||||||
|
<h3 className="text-sm font-medium text-emerald-400 mb-2">
|
||||||
|
Agent Role
|
||||||
|
</h3>
|
||||||
|
<p className="text-neutral-300 text-sm">
|
||||||
|
{agent.role || "Not specified"}
|
||||||
|
</p>
|
||||||
|
<h3 className="text-sm font-medium text-emerald-400 mb-2">
|
||||||
|
Agent Goal
|
||||||
|
</h3>
|
||||||
|
<p className="text-neutral-300 text-sm">
|
||||||
|
{agent.goal || "Not specified"}
|
||||||
|
</p>
|
||||||
|
{/* agent instructions: agent.instruction */}
|
||||||
|
<h3 className="text-sm font-medium text-emerald-400 mb-2">
|
||||||
|
Agent Instructions
|
||||||
|
</h3>
|
||||||
|
<div className="bg-neutral-900 p-3 rounded-md border border-neutral-700 max-h-[200px] overflow-y-auto">
|
||||||
|
<pre className="text-xs text-neutral-300 whitespace-pre-wrap font-mono">
|
||||||
|
{agent.instruction || "No instructions provided"}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="tools" className="mt-0 space-y-4">
|
||||||
|
{getToolsCount() > 0 ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Built-in tools */}
|
||||||
|
{agent.config?.tools && agent.config.tools.length > 0 && (
|
||||||
|
<div className="bg-neutral-800 p-4 rounded-md border border-neutral-700">
|
||||||
|
<h3 className="text-sm font-medium text-emerald-400 mb-2">
|
||||||
|
Built-in Tools
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{agent.config.tools.map((tool, index) => (
|
||||||
|
<Badge
|
||||||
|
key={index}
|
||||||
|
variant="outline"
|
||||||
|
className="bg-neutral-900 border-neutral-700 text-neutral-300 p-2 justify-start"
|
||||||
|
>
|
||||||
|
<TagIcon className="h-3.5 w-3.5 mr-1.5 text-emerald-400" />
|
||||||
|
{typeof tool === 'string' ? tool : 'Custom Tool'}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Agent Tools */}
|
||||||
|
{agent.config?.agent_tools && agent.config.agent_tools.length > 0 && (
|
||||||
|
<div className="bg-neutral-800 p-4 rounded-md border border-neutral-700">
|
||||||
|
<h3 className="text-sm font-medium text-emerald-400 mb-2">
|
||||||
|
Agent Tools
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{agent.config.agent_tools.map((agentId, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="bg-neutral-900 p-2 rounded-md border border-neutral-700 flex items-center"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Bot className="h-3.5 w-3.5 mr-2 text-emerald-400" />
|
||||||
|
<span className="text-sm text-neutral-300">
|
||||||
|
{agents.find(a => a.id === agentId)?.name || agentId}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Custom HTTP tools */}
|
||||||
|
{agent.config?.custom_tools?.http_tools &&
|
||||||
|
agent.config.custom_tools.http_tools.length > 0 && (
|
||||||
|
<div className="bg-neutral-800 p-4 rounded-md border border-neutral-700">
|
||||||
|
<h3 className="text-sm font-medium text-emerald-400 mb-2">
|
||||||
|
Custom HTTP Tools
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{agent.config.custom_tools.http_tools.map(
|
||||||
|
(tool, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="bg-neutral-900 p-2 rounded-md border border-neutral-700 flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Server className="h-3.5 w-3.5 mr-2 text-emerald-400" />
|
||||||
|
<span className="text-sm text-neutral-300">
|
||||||
|
{tool.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs bg-neutral-800 border-neutral-700 text-emerald-400"
|
||||||
|
>
|
||||||
|
{tool.method}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-neutral-800 p-6 rounded-md border border-neutral-700 text-center">
|
||||||
|
<TagIcon className="h-8 w-8 text-neutral-600 mx-auto mb-2" />
|
||||||
|
<p className="text-neutral-400">
|
||||||
|
This agent has no tools configured
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="config" className="mt-0 space-y-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* MCP Servers */}
|
||||||
|
<div className="bg-neutral-800 p-4 rounded-md border border-neutral-700">
|
||||||
|
<h3 className="text-sm font-medium text-emerald-400 mb-2">
|
||||||
|
MCP Servers
|
||||||
|
</h3>
|
||||||
|
{getMCPServersCount() > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{agent.config?.mcp_servers &&
|
||||||
|
agent.config.mcp_servers.map((mcp, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="bg-neutral-900 p-2 rounded-md border border-neutral-700 flex items-center"
|
||||||
|
>
|
||||||
|
<Server className="h-3.5 w-3.5 mr-2 text-emerald-400" />
|
||||||
|
<span className="text-sm text-neutral-300">
|
||||||
|
{mcp.id}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{agent.config?.custom_mcp_servers &&
|
||||||
|
agent.config.custom_mcp_servers.map((mcp, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="bg-neutral-900 p-2 rounded-md border border-neutral-700 flex items-center"
|
||||||
|
>
|
||||||
|
<Server className="h-3.5 w-3.5 mr-2 text-yellow-400" />
|
||||||
|
<span className="text-sm text-neutral-300">
|
||||||
|
{mcp.url}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-neutral-400 text-sm">
|
||||||
|
No MCP servers configured
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* API Key */}
|
||||||
|
<div className="bg-neutral-800 p-4 rounded-md border border-neutral-700">
|
||||||
|
<h3 className="text-sm font-medium text-emerald-400 mb-2">
|
||||||
|
API Key
|
||||||
|
</h3>
|
||||||
|
<p className="text-neutral-300 text-sm">
|
||||||
|
{agent.api_key_id
|
||||||
|
? `Key ID: ${agent.api_key_id}`
|
||||||
|
: "No API key configured"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</ScrollArea>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<DialogFooter className="pt-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="bg-neutral-800 hover:bg-neutral-700 border-neutral-700 text-neutral-300"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Agent Edit Form Dialog */}
|
||||||
|
{isAgentFormOpen && agent && (
|
||||||
|
<AgentForm
|
||||||
|
open={isAgentFormOpen}
|
||||||
|
onOpenChange={setIsAgentFormOpen}
|
||||||
|
initialValues={agent}
|
||||||
|
apiKeys={apiKeys}
|
||||||
|
availableModels={availableModels}
|
||||||
|
availableMCPs={availableMCPs}
|
||||||
|
agents={agents}
|
||||||
|
onOpenApiKeysDialog={() => {}}
|
||||||
|
onOpenMCPDialog={() => {}}
|
||||||
|
onOpenCustomMCPDialog={() => {}}
|
||||||
|
onSave={handleSaveAgent}
|
||||||
|
isLoading={isLoading}
|
||||||
|
getAgentNameById={(id) => id}
|
||||||
|
clientId={clientId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
158
frontend/app/chat/components/AttachedFiles.tsx
Normal file
158
frontend/app/chat/components/AttachedFiles.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/chat/components/AttachedFiles.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: August 24, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { formatFileSize, isImageFile } from "@/lib/file-utils";
|
||||||
|
import { File, FileText, Download, Image } from "lucide-react";
|
||||||
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
interface AttachedFile {
|
||||||
|
filename: string;
|
||||||
|
content_type: string;
|
||||||
|
data?: string;
|
||||||
|
size?: number;
|
||||||
|
preview_url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AttachedFilesProps {
|
||||||
|
files: AttachedFile[];
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AttachedFiles({ files, className = "" }: AttachedFilesProps) {
|
||||||
|
if (!files || files.length === 0) return null;
|
||||||
|
|
||||||
|
const downloadFile = (file: AttachedFile) => {
|
||||||
|
if (!file.data) {
|
||||||
|
toast({
|
||||||
|
title: "File without data for download",
|
||||||
|
description: file.filename,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const link = document.createElement("a");
|
||||||
|
const dataUrl = file.data.startsWith("data:")
|
||||||
|
? file.data
|
||||||
|
: `data:${file.content_type};base64,${file.data}`;
|
||||||
|
|
||||||
|
link.href = dataUrl;
|
||||||
|
link.download = file.filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error downloading file",
|
||||||
|
description: file.filename,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col gap-2 mt-2 ${className}`}>
|
||||||
|
<div className="text-xs text-neutral-400 mb-1">Attached files:</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{files
|
||||||
|
.map((file, index) => {
|
||||||
|
if (!file.data) {
|
||||||
|
toast({
|
||||||
|
title: "File without data for display",
|
||||||
|
description: file.filename,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex flex-col bg-[#333] rounded-md overflow-hidden border border-[#444] hover:border-[#666] transition-colors"
|
||||||
|
>
|
||||||
|
{isImageFile(file.content_type) && file.data && (
|
||||||
|
<div className="w-full max-w-[200px] h-[120px] bg-black flex items-center justify-center">
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
file.preview_url ||
|
||||||
|
(file.data.startsWith("data:")
|
||||||
|
? file.data
|
||||||
|
: `data:${file.content_type};base64,${file.data}`)
|
||||||
|
}
|
||||||
|
alt={file.filename}
|
||||||
|
className="max-w-full max-h-full object-contain"
|
||||||
|
onError={(e) => {
|
||||||
|
toast({
|
||||||
|
title: "Error loading image",
|
||||||
|
description: file.filename,
|
||||||
|
});
|
||||||
|
(e.target as HTMLImageElement).src =
|
||||||
|
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZjY2NjYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS1pbWFnZS1vZmYiPjxsaW5lIHgxPSIyIiB5MT0iMiIgeDI9IjIyIiB5Mj0iMjIiLz48PHJlY3QgdyA";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="p-2 flex items-center gap-2">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
{isImageFile(file.content_type) ? (
|
||||||
|
<Image className="h-4 w-4 text-emerald-400" />
|
||||||
|
) : file.content_type === "application/pdf" ? (
|
||||||
|
<FileText className="h-4 w-4 text-emerald-400" />
|
||||||
|
) : (
|
||||||
|
<File className="h-4 w-4 text-emerald-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-xs font-medium truncate max-w-[150px]">
|
||||||
|
{file.filename}
|
||||||
|
</div>
|
||||||
|
{file.size && (
|
||||||
|
<div className="text-[10px] text-neutral-400">
|
||||||
|
{formatFileSize(file.size)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{file.data && (
|
||||||
|
<button
|
||||||
|
onClick={() => downloadFile(file)}
|
||||||
|
className="text-emerald-400 hover:text-white transition-colors"
|
||||||
|
title="Download"
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.filter(Boolean)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
190
frontend/app/chat/components/ChatContainer.tsx
Normal file
190
frontend/app/chat/components/ChatContainer.tsx
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/chat/components/ChatContainer.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 14, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useRef, useEffect, useState } from "react";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { MessageSquare, Loader2, Bot, Zap } from "lucide-react";
|
||||||
|
import { ChatMessage } from "./ChatMessage";
|
||||||
|
import { ChatInput } from "./ChatInput";
|
||||||
|
import { ChatMessage as ChatMessageType } from "@/services/sessionService";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface FunctionMessageContent {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
author?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatContainerProps {
|
||||||
|
messages: ChatMessageType[];
|
||||||
|
isLoading: boolean;
|
||||||
|
onSendMessage: (message: string) => void;
|
||||||
|
agentColor: string;
|
||||||
|
expandedFunctions: Record<string, boolean>;
|
||||||
|
toggleFunctionExpansion: (messageId: string) => void;
|
||||||
|
containsMarkdown: (text: string) => boolean;
|
||||||
|
getMessageText: (message: ChatMessageType) => string | FunctionMessageContent;
|
||||||
|
agentName?: string;
|
||||||
|
containerClassName?: string;
|
||||||
|
messagesContainerClassName?: string;
|
||||||
|
inputContainerClassName?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatContainer({
|
||||||
|
messages,
|
||||||
|
isLoading,
|
||||||
|
onSendMessage,
|
||||||
|
agentColor,
|
||||||
|
expandedFunctions,
|
||||||
|
toggleFunctionExpansion,
|
||||||
|
containsMarkdown,
|
||||||
|
getMessageText,
|
||||||
|
agentName = "Agent",
|
||||||
|
containerClassName = "",
|
||||||
|
messagesContainerClassName = "",
|
||||||
|
inputContainerClassName = "",
|
||||||
|
sessionId,
|
||||||
|
}: ChatContainerProps) {
|
||||||
|
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isInitializing, setIsInitializing] = useState(false);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
if (messagesContainerRef.current) {
|
||||||
|
messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (messages.length > 0) {
|
||||||
|
setTimeout(scrollToBottom, 100);
|
||||||
|
}
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
// Simulate initial loading for smoother UX
|
||||||
|
useEffect(() => {
|
||||||
|
if (sessionId) {
|
||||||
|
setIsInitializing(true);
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsInitializing(false);
|
||||||
|
}, 1000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [sessionId]);
|
||||||
|
|
||||||
|
const isEmpty = messages.length === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn(
|
||||||
|
"flex-1 flex flex-col overflow-hidden bg-gradient-to-b from-neutral-900 to-neutral-950",
|
||||||
|
containerClassName
|
||||||
|
)}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex-1 overflow-hidden p-5",
|
||||||
|
messagesContainerClassName
|
||||||
|
)}
|
||||||
|
style={{ filter: isLoading && !isInitializing ? "blur(1px)" : "none" }}
|
||||||
|
>
|
||||||
|
<ScrollArea
|
||||||
|
ref={messagesContainerRef}
|
||||||
|
className="h-full pr-4"
|
||||||
|
>
|
||||||
|
{isInitializing ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full">
|
||||||
|
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-emerald-500 to-emerald-700 flex items-center justify-center shadow-lg mb-4 animate-pulse">
|
||||||
|
<Zap className="h-5 w-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<p className="text-neutral-400 mb-2">Loading conversation...</p>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-bounce"
|
||||||
|
style={{ animationDelay: '0ms' }}></span>
|
||||||
|
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-bounce"
|
||||||
|
style={{ animationDelay: '150ms' }}></span>
|
||||||
|
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-bounce"
|
||||||
|
style={{ animationDelay: '300ms' }}></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : isEmpty ? (
|
||||||
|
<div className="h-full flex flex-col items-center justify-center text-center p-6">
|
||||||
|
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-blue-500/20 to-emerald-500/20 flex items-center justify-center shadow-lg mb-5 border border-emerald-500/30">
|
||||||
|
<MessageSquare className="h-6 w-6 text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium text-neutral-300 mb-2">
|
||||||
|
{`Chat with ${agentName}`}
|
||||||
|
</h3>
|
||||||
|
<p className="text-neutral-500 text-sm max-w-md">
|
||||||
|
Type your message below to start the conversation. This chat will help you interact with the agent and explore its capabilities.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-6 py-4 flex-1">
|
||||||
|
{messages.map((message) => {
|
||||||
|
const messageContent = getMessageText(message);
|
||||||
|
const isExpanded = expandedFunctions[message.id] || false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChatMessage
|
||||||
|
key={message.id}
|
||||||
|
message={message}
|
||||||
|
agentColor={agentColor}
|
||||||
|
isExpanded={isExpanded}
|
||||||
|
toggleExpansion={toggleFunctionExpansion}
|
||||||
|
containsMarkdown={containsMarkdown}
|
||||||
|
messageContent={messageContent}
|
||||||
|
sessionId={sessionId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={cn(
|
||||||
|
"p-3 border-t border-neutral-800 bg-neutral-900",
|
||||||
|
inputContainerClassName
|
||||||
|
)}>
|
||||||
|
{isLoading && !isInitializing && (
|
||||||
|
<div className="px-4 py-2 mb-3 rounded-lg bg-neutral-800/50 text-sm text-neutral-400 flex items-center">
|
||||||
|
<Loader2 className="h-3 w-3 mr-2 animate-spin text-emerald-400" />
|
||||||
|
Agent is thinking...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ChatInput
|
||||||
|
onSendMessage={onSendMessage}
|
||||||
|
isLoading={isLoading}
|
||||||
|
placeholder="Type your message..."
|
||||||
|
autoFocus={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
270
frontend/app/chat/components/ChatInput.tsx
Normal file
270
frontend/app/chat/components/ChatInput.tsx
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/chat/components/ChatInput.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 14, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useRef } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Loader2, Send, Paperclip, X, Image, FileText, File } from "lucide-react";
|
||||||
|
import { FileData, formatFileSize, isImageFile } from "@/lib/file-utils";
|
||||||
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
interface ChatInputProps {
|
||||||
|
onSendMessage: (message: string, files?: FileData[]) => void;
|
||||||
|
isLoading?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
buttonClassName?: string;
|
||||||
|
containerClassName?: string;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatInput({
|
||||||
|
onSendMessage,
|
||||||
|
isLoading = false,
|
||||||
|
placeholder = "Type your message...",
|
||||||
|
className = "",
|
||||||
|
buttonClassName = "",
|
||||||
|
containerClassName = "",
|
||||||
|
autoFocus = true,
|
||||||
|
}: ChatInputProps) {
|
||||||
|
const [messageInput, setMessageInput] = useState("");
|
||||||
|
const [selectedFiles, setSelectedFiles] = useState<FileData[]>([]);
|
||||||
|
const [resetFileUpload, setResetFileUpload] = useState(false);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
// Autofocus the textarea when the component is mounted
|
||||||
|
React.useEffect(() => {
|
||||||
|
// Small timeout to ensure focus is applied after the complete rendering
|
||||||
|
if (autoFocus) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (textareaRef.current && !isLoading) {
|
||||||
|
textareaRef.current.focus();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [isLoading, autoFocus]);
|
||||||
|
|
||||||
|
const handleSendMessage = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!messageInput.trim() && selectedFiles.length === 0) return;
|
||||||
|
|
||||||
|
onSendMessage(messageInput, selectedFiles.length > 0 ? selectedFiles : undefined);
|
||||||
|
|
||||||
|
setMessageInput("");
|
||||||
|
setSelectedFiles([]);
|
||||||
|
|
||||||
|
setResetFileUpload(true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setResetFileUpload(false);
|
||||||
|
// Keep the focus on the textarea after sending the message
|
||||||
|
if (autoFocus && textareaRef.current) {
|
||||||
|
textareaRef.current.focus();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
const textarea = document.querySelector("textarea");
|
||||||
|
if (textarea) textarea.style.height = "auto";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSendMessage(e as unknown as React.FormEvent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const autoResizeTextarea = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const textarea = e.target;
|
||||||
|
textarea.style.height = "auto";
|
||||||
|
const maxHeight = 10 * 24;
|
||||||
|
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
|
||||||
|
textarea.style.height = `${newHeight}px`;
|
||||||
|
setMessageInput(textarea.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilesSelected = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (!e.target.files || e.target.files.length === 0) return;
|
||||||
|
|
||||||
|
const newFiles = Array.from(e.target.files);
|
||||||
|
const maxFileSize = 10 * 1024 * 1024; // 10MB
|
||||||
|
|
||||||
|
if (selectedFiles.length + newFiles.length > 5) {
|
||||||
|
toast({
|
||||||
|
title: `You can only attach up to 5 files.`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validFiles: FileData[] = [];
|
||||||
|
|
||||||
|
for (const file of newFiles) {
|
||||||
|
if (file.size > maxFileSize) {
|
||||||
|
toast({
|
||||||
|
title: `File ${file.name} exceeds the maximum size of ${formatFileSize(maxFileSize)}.`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
const readFile = new Promise<string>((resolve, reject) => {
|
||||||
|
reader.onload = () => {
|
||||||
|
const base64 = reader.result as string;
|
||||||
|
const base64Data = base64.split(',')[1];
|
||||||
|
resolve(base64Data);
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
|
||||||
|
const base64Data = await readFile;
|
||||||
|
const previewUrl = URL.createObjectURL(file);
|
||||||
|
|
||||||
|
validFiles.push({
|
||||||
|
filename: file.name,
|
||||||
|
content_type: file.type,
|
||||||
|
data: base64Data,
|
||||||
|
size: file.size,
|
||||||
|
preview_url: previewUrl
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing file:", error);
|
||||||
|
toast({
|
||||||
|
title: `Error processing file ${file.name}`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validFiles.length > 0) {
|
||||||
|
const updatedFiles = [...selectedFiles, ...validFiles];
|
||||||
|
setSelectedFiles(updatedFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openFileSelector = () => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col w-full ${containerClassName}`}>
|
||||||
|
{selectedFiles.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 px-2 mb-3 mt-1">
|
||||||
|
{selectedFiles.map((file, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center gap-1.5 bg-gradient-to-br from-neutral-800 to-neutral-900 text-white rounded-lg p-2 text-xs border border-neutral-700/50 shadow-sm"
|
||||||
|
>
|
||||||
|
{isImageFile(file.content_type) ? (
|
||||||
|
<Image className="h-4 w-4 text-emerald-400" />
|
||||||
|
) : file.content_type === 'application/pdf' ? (
|
||||||
|
<FileText className="h-4 w-4 text-emerald-400" />
|
||||||
|
) : (
|
||||||
|
<File className="h-4 w-4 text-emerald-400" />
|
||||||
|
)}
|
||||||
|
<span className="max-w-[120px] truncate">{file.filename}</span>
|
||||||
|
<span className="text-neutral-400">({formatFileSize(file.size)})</span>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const updatedFiles = selectedFiles.filter((_, i) => i !== index);
|
||||||
|
setSelectedFiles(updatedFiles);
|
||||||
|
}}
|
||||||
|
className="ml-1 text-neutral-400 hover:text-white transition-colors bg-neutral-700/30 rounded-full p-1"
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={handleSendMessage}
|
||||||
|
className="flex w-full items-center gap-2 px-2"
|
||||||
|
>
|
||||||
|
{selectedFiles.length < 5 && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
openFileSelector();
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
className="flex items-center justify-center w-9 h-9 rounded-full hover:bg-neutral-800/60 text-neutral-400 hover:text-emerald-400 transition-all border border-neutral-700/30"
|
||||||
|
title="Attach file"
|
||||||
|
>
|
||||||
|
<Paperclip className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
value={messageInput}
|
||||||
|
onChange={autoResizeTextarea}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className={`flex-1 bg-neutral-800/40 border-neutral-700/50 text-white focus-visible:ring-emerald-500/50 focus-visible:border-emerald-500/50 min-h-[40px] max-h-[240px] resize-none rounded-xl ${className}`}
|
||||||
|
disabled={isLoading}
|
||||||
|
rows={1}
|
||||||
|
ref={textareaRef}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading || (!messageInput.trim() && selectedFiles.length === 0)}
|
||||||
|
className={`bg-gradient-to-r from-emerald-500 to-emerald-600 text-white hover:from-emerald-600 hover:to-emerald-700 rounded-full shadow-md h-9 w-9 p-0 ${buttonClassName}`}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Send className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleFilesSelected}
|
||||||
|
className="hidden"
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
375
frontend/app/chat/components/ChatMessage.tsx
Normal file
375
frontend/app/chat/components/ChatMessage.tsx
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/chat/components/ChatMessage.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
|
import { ChatMessage as ChatMessageType } from "@/services/sessionService";
|
||||||
|
import { ChevronDown, ChevronRight, Copy, Check, User, Bot, Terminal } from "lucide-react";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { InlineDataAttachments } from "./InlineDataAttachments";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface FunctionMessageContent {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
author?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AttachedFile {
|
||||||
|
filename: string;
|
||||||
|
content_type: string;
|
||||||
|
data: string;
|
||||||
|
size: number;
|
||||||
|
preview_url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatMessageProps {
|
||||||
|
message: ChatMessageType;
|
||||||
|
agentColor: string;
|
||||||
|
isExpanded: boolean;
|
||||||
|
toggleExpansion: (messageId: string) => void;
|
||||||
|
containsMarkdown: (text: string) => boolean;
|
||||||
|
messageContent: string | FunctionMessageContent;
|
||||||
|
sessionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatMessage({
|
||||||
|
message,
|
||||||
|
agentColor,
|
||||||
|
isExpanded,
|
||||||
|
toggleExpansion,
|
||||||
|
containsMarkdown,
|
||||||
|
messageContent,
|
||||||
|
sessionId,
|
||||||
|
}: ChatMessageProps) {
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
|
||||||
|
const isUser = message.author === "user";
|
||||||
|
const hasFunctionCall = message.content.parts.some(
|
||||||
|
(part) => part.functionCall || part.function_call
|
||||||
|
);
|
||||||
|
const hasFunctionResponse = message.content.parts.some(
|
||||||
|
(part) => part.functionResponse || part.function_response
|
||||||
|
);
|
||||||
|
const isFunctionMessage = hasFunctionCall || hasFunctionResponse;
|
||||||
|
const isTaskExecutor = typeof messageContent === "object" &&
|
||||||
|
"author" in messageContent &&
|
||||||
|
typeof messageContent.author === "string" &&
|
||||||
|
messageContent.author.endsWith("- Task executor");
|
||||||
|
|
||||||
|
const inlineDataParts = message.content.parts.filter(part => part.inline_data);
|
||||||
|
const hasInlineData = inlineDataParts.length > 0;
|
||||||
|
|
||||||
|
const copyToClipboard = () => {
|
||||||
|
const textToCopy = typeof messageContent === "string"
|
||||||
|
? messageContent
|
||||||
|
: messageContent.content;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||||
|
setIsCopied(true);
|
||||||
|
setTimeout(() => setIsCopied(false), 2000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate appropriate avatar content
|
||||||
|
const getAvatar = () => {
|
||||||
|
if (isUser) {
|
||||||
|
return (
|
||||||
|
<Avatar className="bg-gradient-to-br from-emerald-500 to-emerald-700 shadow-md border-0">
|
||||||
|
<AvatarFallback className="bg-transparent">
|
||||||
|
<User className="h-4 w-4 text-white" />
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Avatar className={`shadow-md border-0 ${
|
||||||
|
isFunctionMessage
|
||||||
|
? "bg-gradient-to-br from-emerald-600 to-emerald-800"
|
||||||
|
: "bg-gradient-to-br from-purple-600 to-purple-800"
|
||||||
|
}`}>
|
||||||
|
<AvatarFallback className="bg-transparent">
|
||||||
|
{isFunctionMessage ?
|
||||||
|
<Terminal className="h-4 w-4 text-white" /> :
|
||||||
|
<Bot className="h-4 w-4 text-white" />
|
||||||
|
}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={message.id}
|
||||||
|
className="flex w-full"
|
||||||
|
style={{
|
||||||
|
justifyContent: isUser ? "flex-end" : "flex-start"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex gap-3 max-w-[90%]"
|
||||||
|
style={{
|
||||||
|
flexDirection: isUser ? "row-reverse" : "row"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getAvatar()}
|
||||||
|
<div
|
||||||
|
className={`rounded-lg p-3 overflow-hidden relative group shadow-md ${
|
||||||
|
isFunctionMessage || isTaskExecutor
|
||||||
|
? "bg-gradient-to-br from-neutral-800 to-neutral-900 border border-neutral-700/50 text-emerald-300 font-mono text-sm"
|
||||||
|
: isUser
|
||||||
|
? "bg-emerald-500 text-white"
|
||||||
|
: "bg-gradient-to-br from-neutral-800 to-neutral-900 border border-neutral-700/50 text-white"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
wordBreak: "break-word",
|
||||||
|
maxWidth: "calc(100% - 3rem)",
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isFunctionMessage || isTaskExecutor ? (
|
||||||
|
<div className="w-full">
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 cursor-pointer hover:bg-[#444] rounded px-1 py-0.5 transition-colors"
|
||||||
|
onClick={() => toggleExpansion(message.id)}
|
||||||
|
>
|
||||||
|
{typeof messageContent === "object" &&
|
||||||
|
"title" in messageContent && (
|
||||||
|
<>
|
||||||
|
<div className="flex-1 font-semibold">
|
||||||
|
{(messageContent as FunctionMessageContent).title}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center w-5 h-5 text-emerald-400">
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isTaskExecutor && (
|
||||||
|
<>
|
||||||
|
<div className="flex-1 font-semibold">
|
||||||
|
Task Execution
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center w-5 h-5 text-emerald-400">
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="mt-2 pt-2 border-t border-[#555]">
|
||||||
|
{typeof messageContent === "object" &&
|
||||||
|
"content" in messageContent && (
|
||||||
|
<div className="max-w-full overflow-x-auto">
|
||||||
|
<pre className="whitespace-pre-wrap text-xs max-w-full" style={{
|
||||||
|
wordWrap: "break-word",
|
||||||
|
maxWidth: "100%",
|
||||||
|
wordBreak: "break-all"
|
||||||
|
}}>
|
||||||
|
{(messageContent as FunctionMessageContent).content}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="markdown-content break-words max-w-full overflow-x-auto">
|
||||||
|
{typeof messageContent === "object" &&
|
||||||
|
"author" in messageContent &&
|
||||||
|
messageContent.author !== "user" &&
|
||||||
|
!isTaskExecutor && (
|
||||||
|
<div className="text-xs text-neutral-400 mb-1">
|
||||||
|
{messageContent.author}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{((typeof messageContent === "string" &&
|
||||||
|
containsMarkdown(messageContent)) ||
|
||||||
|
(typeof messageContent === "object" &&
|
||||||
|
"content" in messageContent &&
|
||||||
|
typeof messageContent.content === "string" &&
|
||||||
|
containsMarkdown(messageContent.content))) &&
|
||||||
|
!isTaskExecutor ? (
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
components={{
|
||||||
|
h1: ({ ...props }) => (
|
||||||
|
<h1 className="text-xl font-bold my-4" {...props} />
|
||||||
|
),
|
||||||
|
h2: ({ ...props }) => (
|
||||||
|
<h2 className="text-lg font-bold my-3" {...props} />
|
||||||
|
),
|
||||||
|
h3: ({ ...props }) => (
|
||||||
|
<h3 className="text-base font-bold my-2" {...props} />
|
||||||
|
),
|
||||||
|
h4: ({ ...props }) => (
|
||||||
|
<h4 className="font-semibold my-2" {...props} />
|
||||||
|
),
|
||||||
|
p: ({ ...props }) => <p className="mb-3" {...props} />,
|
||||||
|
ul: ({ ...props }) => (
|
||||||
|
<ul
|
||||||
|
className="list-disc pl-6 mb-3 space-y-1"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
ol: ({ ...props }) => (
|
||||||
|
<ol
|
||||||
|
className="list-decimal pl-6 mb-3 space-y-1"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
li: ({ ...props }) => <li className="mb-1" {...props} />,
|
||||||
|
a: ({ ...props }) => (
|
||||||
|
<a
|
||||||
|
className="text-emerald-300 underline hover:opacity-80 transition-opacity"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
blockquote: ({ ...props }) => (
|
||||||
|
<blockquote
|
||||||
|
className="border-l-4 border-[#444] pl-4 py-1 italic my-3 text-neutral-300"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
code: ({ className, children, ...props }: any) => {
|
||||||
|
const match = /language-(\w+)/.exec(className || "");
|
||||||
|
const isInline =
|
||||||
|
!match &&
|
||||||
|
typeof children === "string" &&
|
||||||
|
!children.includes("\n");
|
||||||
|
|
||||||
|
if (isInline) {
|
||||||
|
return (
|
||||||
|
<code
|
||||||
|
className="bg-[#333] px-1.5 py-0.5 rounded text-emerald-300 text-sm font-mono"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="my-3 relative group/code">
|
||||||
|
<div className="bg-[#1a1a1a] rounded-t-md border-b border-[#333] p-2 text-xs text-neutral-400 flex justify-between items-center">
|
||||||
|
<span>{match?.[1] || "Code"}</span>
|
||||||
|
<button
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
className="text-neutral-400 hover:text-emerald-300 transition-colors"
|
||||||
|
title="Copy code"
|
||||||
|
>
|
||||||
|
{isCopied ? (
|
||||||
|
<Check className="h-3.5 w-3.5" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre className="bg-[#1a1a1a] p-3 rounded-b-md overflow-x-auto whitespace-pre text-sm">
|
||||||
|
<code {...props}>{children}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
table: ({ ...props }) => (
|
||||||
|
<div className="overflow-x-auto my-3">
|
||||||
|
<table
|
||||||
|
className="min-w-full border border-[#333] rounded"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
thead: ({ ...props }) => (
|
||||||
|
<thead className="bg-[#1a1a1a]" {...props} />
|
||||||
|
),
|
||||||
|
tbody: ({ ...props }) => <tbody {...props} />,
|
||||||
|
tr: ({ ...props }) => (
|
||||||
|
<tr
|
||||||
|
className="border-b border-[#333] last:border-0"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
th: ({ ...props }) => (
|
||||||
|
<th
|
||||||
|
className="px-4 py-2 text-left text-xs font-semibold text-neutral-300"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
td: ({ ...props }) => (
|
||||||
|
<td className="px-4 py-2 text-sm" {...props} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{typeof messageContent === "string"
|
||||||
|
? messageContent
|
||||||
|
: messageContent.content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
) : (
|
||||||
|
<div className="whitespace-pre-wrap">
|
||||||
|
{typeof messageContent === "string"
|
||||||
|
? messageContent
|
||||||
|
: messageContent.content}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasInlineData && (
|
||||||
|
<InlineDataAttachments parts={inlineDataParts} sessionId={sessionId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
className="absolute top-2 right-2 p-1.5 rounded-full bg-neutral-800/80 text-neutral-400 hover:text-white opacity-0 group-hover:opacity-100 transition-all hover:bg-neutral-700/80"
|
||||||
|
title="Copy message"
|
||||||
|
>
|
||||||
|
{isCopied ? (
|
||||||
|
<Check className="h-3.5 w-3.5" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
185
frontend/app/chat/components/FileUpload.tsx
Normal file
185
frontend/app/chat/components/FileUpload.tsx
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/chat/components/FileUpload.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: August 24, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
|
import { FileData, formatFileSize, isImageFile } from "@/lib/file-utils";
|
||||||
|
import { Paperclip, X, Image, File, FileText } from "lucide-react";
|
||||||
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
interface FileUploadProps {
|
||||||
|
onFilesSelected: (files: FileData[]) => void;
|
||||||
|
maxFileSize?: number;
|
||||||
|
maxFiles?: number;
|
||||||
|
className?: string;
|
||||||
|
reset?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FileUpload({
|
||||||
|
onFilesSelected,
|
||||||
|
maxFileSize = 10 * 1024 * 1024, // 10MB
|
||||||
|
maxFiles = 5,
|
||||||
|
className = "",
|
||||||
|
reset = false, // Default false
|
||||||
|
}: FileUploadProps) {
|
||||||
|
const [selectedFiles, setSelectedFiles] = useState<FileData[]>([]);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (reset && selectedFiles.length > 0) {
|
||||||
|
setSelectedFiles([]);
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [reset]);
|
||||||
|
|
||||||
|
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (!e.target.files || e.target.files.length === 0) return;
|
||||||
|
|
||||||
|
const newFiles = Array.from(e.target.files);
|
||||||
|
|
||||||
|
if (selectedFiles.length + newFiles.length > maxFiles) {
|
||||||
|
toast({
|
||||||
|
title: `You can only attach up to ${maxFiles} files.`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validFiles: FileData[] = [];
|
||||||
|
|
||||||
|
for (const file of newFiles) {
|
||||||
|
if (file.size > maxFileSize) {
|
||||||
|
toast({
|
||||||
|
title: `File ${file.name} exceeds the maximum size of ${formatFileSize(maxFileSize)}.`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
const readFile = new Promise<string>((resolve, reject) => {
|
||||||
|
reader.onload = () => {
|
||||||
|
const base64 = reader.result as string;
|
||||||
|
const base64Data = base64.split(',')[1];
|
||||||
|
resolve(base64Data);
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
|
||||||
|
const base64Data = await readFile;
|
||||||
|
const previewUrl = URL.createObjectURL(file);
|
||||||
|
|
||||||
|
validFiles.push({
|
||||||
|
filename: file.name,
|
||||||
|
content_type: file.type,
|
||||||
|
data: base64Data,
|
||||||
|
size: file.size,
|
||||||
|
preview_url: previewUrl
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing file:", error);
|
||||||
|
toast({
|
||||||
|
title: `Error processing file ${file.name}`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validFiles.length > 0) {
|
||||||
|
const updatedFiles = [...selectedFiles, ...validFiles];
|
||||||
|
setSelectedFiles(updatedFiles);
|
||||||
|
onFilesSelected(updatedFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFile = (index: number) => {
|
||||||
|
const updatedFiles = selectedFiles.filter((_, i) => i !== index);
|
||||||
|
setSelectedFiles(updatedFiles);
|
||||||
|
onFilesSelected(updatedFiles);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex gap-2 items-center ${className}`}>
|
||||||
|
{selectedFiles.length > 0 && (
|
||||||
|
<div className="flex gap-2 flex-wrap items-center flex-1">
|
||||||
|
{selectedFiles.map((file, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center gap-1 bg-[#333] text-white rounded-md p-1.5 text-xs group relative"
|
||||||
|
>
|
||||||
|
{isImageFile(file.content_type) ? (
|
||||||
|
<Image className="h-4 w-4 text-emerald-400" />
|
||||||
|
) : file.content_type === 'application/pdf' ? (
|
||||||
|
<FileText className="h-4 w-4 text-emerald-400" />
|
||||||
|
) : (
|
||||||
|
<File className="h-4 w-4 text-emerald-400" />
|
||||||
|
)}
|
||||||
|
<span className="max-w-[120px] truncate">{file.filename}</span>
|
||||||
|
<span className="text-neutral-400">({formatFileSize(file.size)})</span>
|
||||||
|
<button
|
||||||
|
onClick={() => removeFile(index)}
|
||||||
|
className="ml-1 text-neutral-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedFiles.length < maxFiles && (
|
||||||
|
<button
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
type="button"
|
||||||
|
className="flex items-center justify-center w-8 h-8 rounded-full hover:bg-[#333] text-neutral-400 hover:text-emerald-400 transition-colors"
|
||||||
|
title="Attach file"
|
||||||
|
>
|
||||||
|
<Paperclip className="h-5 w-5" />
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
className="hidden"
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
182
frontend/app/chat/components/InlineDataAttachments.tsx
Normal file
182
frontend/app/chat/components/InlineDataAttachments.tsx
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/chat/components/InlineDataAttachments.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: August 29, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { formatFileSize, isImageFile } from "@/lib/file-utils";
|
||||||
|
import { File, FileText, Download, Image } from "lucide-react";
|
||||||
|
import { ChatPart } from "@/services/sessionService";
|
||||||
|
|
||||||
|
interface InlineDataAttachmentsProps {
|
||||||
|
parts: ChatPart[];
|
||||||
|
className?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProcessedFile {
|
||||||
|
filename: string;
|
||||||
|
content_type: string;
|
||||||
|
data: string;
|
||||||
|
size: number;
|
||||||
|
preview_url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InlineDataAttachments({ parts, className = "", sessionId }: InlineDataAttachmentsProps) {
|
||||||
|
const [processedFiles, setProcessedFiles] = useState<ProcessedFile[]>([]);
|
||||||
|
const [isProcessed, setIsProcessed] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isProcessed) return;
|
||||||
|
|
||||||
|
const validParts = parts.filter(part => part.inline_data && part.inline_data.data);
|
||||||
|
|
||||||
|
if (validParts.length === 0) {
|
||||||
|
setIsProcessed(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = validParts.map((part, index) => {
|
||||||
|
const { mime_type, data } = part.inline_data!;
|
||||||
|
const extension = mime_type.split('/')[1] || 'file';
|
||||||
|
|
||||||
|
let filename = '';
|
||||||
|
|
||||||
|
if (part.inline_data?.metadata?.filename) {
|
||||||
|
filename = part.inline_data.metadata.filename;
|
||||||
|
}
|
||||||
|
else if (part.file_data?.filename) {
|
||||||
|
filename = part.file_data.filename;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
filename = `media_${index + 1}.${extension}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let preview_url = undefined;
|
||||||
|
if (data && isImageFile(mime_type)) {
|
||||||
|
preview_url = data.startsWith('data:')
|
||||||
|
? data
|
||||||
|
: `data:${mime_type};base64,${data}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileData: ProcessedFile = {
|
||||||
|
filename,
|
||||||
|
content_type: mime_type,
|
||||||
|
size: data.length,
|
||||||
|
data,
|
||||||
|
preview_url
|
||||||
|
};
|
||||||
|
|
||||||
|
return fileData;
|
||||||
|
});
|
||||||
|
|
||||||
|
setProcessedFiles(files);
|
||||||
|
setIsProcessed(true);
|
||||||
|
}, [parts, isProcessed]);
|
||||||
|
|
||||||
|
if (processedFiles.length === 0) return null;
|
||||||
|
|
||||||
|
const downloadFile = (file: ProcessedFile) => {
|
||||||
|
try {
|
||||||
|
const link = document.createElement("a");
|
||||||
|
const dataUrl = file.data.startsWith('data:')
|
||||||
|
? file.data
|
||||||
|
: `data:${file.content_type};base64,${file.data}`;
|
||||||
|
|
||||||
|
link.href = dataUrl;
|
||||||
|
link.download = file.filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error downloading file ${file.filename}:`, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFileUrl = (file: ProcessedFile) => {
|
||||||
|
return file.preview_url || (file.data.startsWith('data:')
|
||||||
|
? file.data
|
||||||
|
: `data:${file.content_type};base64,${file.data}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col gap-2 mt-2 ${className}`}>
|
||||||
|
<div className="text-xs text-neutral-400 mb-1">
|
||||||
|
<span>Attached files:</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{processedFiles.map((file, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex flex-col bg-[#333] rounded-md overflow-hidden border border-[#444] hover:border-[#666] transition-colors"
|
||||||
|
>
|
||||||
|
{isImageFile(file.content_type) && (
|
||||||
|
<div className="w-full max-w-[200px] h-[120px] bg-black flex items-center justify-center">
|
||||||
|
<img
|
||||||
|
src={getFileUrl(file)}
|
||||||
|
alt={file.filename}
|
||||||
|
className="max-w-full max-h-full object-contain"
|
||||||
|
onError={(e) => {
|
||||||
|
console.error(`Error loading image ${file.filename}`);
|
||||||
|
(e.target as HTMLImageElement).src = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZjY2NjYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS1pbWFnZS1vZmYiPjxsaW5lIHgxPSIyIiB5MT0iMiIgeDI9IjIyIiB5Mj0iMjIiLz48cmVjdCB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHg9IjIiIHk9IjIiIHJ4PSIyIiByeT0iMiIvPjxsaW5lIHgxPSI4IiB5MT0iMTAiIHgyPSI4IiB5Mj0iMTAiLz48bGluZSB4MT0iMTIiIHkxPSIxNCIgeDI9IjEyIiB5Mj0iMTQiLz48L3N2Zz4=";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="p-2 flex items-center gap-2">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
{isImageFile(file.content_type) ? (
|
||||||
|
<Image className="h-4 w-4 text-emerald-400" />
|
||||||
|
) : file.content_type === "application/pdf" ? (
|
||||||
|
<FileText className="h-4 w-4 text-emerald-400" />
|
||||||
|
) : (
|
||||||
|
<File className="h-4 w-4 text-emerald-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-xs font-medium truncate max-w-[150px]">
|
||||||
|
{file.filename}
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-neutral-400">
|
||||||
|
{formatFileSize(file.size)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => downloadFile(file)}
|
||||||
|
className="text-emerald-400 hover:text-white transition-colors"
|
||||||
|
title="Download"
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
248
frontend/app/chat/components/SessionList.tsx
Normal file
248
frontend/app/chat/components/SessionList.tsx
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/chat/components/SessionList.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Search, Filter, Plus, Loader2 } from "lucide-react";
|
||||||
|
import { ChatSession } from "@/services/sessionService";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
|
interface SessionListProps {
|
||||||
|
sessions: ChatSession[];
|
||||||
|
agents: any[];
|
||||||
|
selectedSession: string | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
searchTerm: string;
|
||||||
|
selectedAgentFilter: string;
|
||||||
|
showAgentFilter: boolean;
|
||||||
|
setSearchTerm: (value: string) => void;
|
||||||
|
setSelectedAgentFilter: (value: string) => void;
|
||||||
|
setShowAgentFilter: (value: boolean) => void;
|
||||||
|
setSelectedSession: (value: string | null) => void;
|
||||||
|
setIsNewChatDialogOpen: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SessionList({
|
||||||
|
sessions,
|
||||||
|
agents,
|
||||||
|
selectedSession,
|
||||||
|
isLoading,
|
||||||
|
searchTerm,
|
||||||
|
selectedAgentFilter,
|
||||||
|
showAgentFilter,
|
||||||
|
setSearchTerm,
|
||||||
|
setSelectedAgentFilter,
|
||||||
|
setShowAgentFilter,
|
||||||
|
setSelectedSession,
|
||||||
|
setIsNewChatDialogOpen,
|
||||||
|
}: SessionListProps) {
|
||||||
|
const filteredSessions = sessions.filter((session) => {
|
||||||
|
const matchesSearchTerm = session.id
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchTerm.toLowerCase());
|
||||||
|
|
||||||
|
if (selectedAgentFilter === "all") {
|
||||||
|
return matchesSearchTerm;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionAgentId = session.id.split("_")[1];
|
||||||
|
return matchesSearchTerm && sessionAgentId === selectedAgentFilter;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedSessions = [...filteredSessions].sort((a, b) => {
|
||||||
|
const updateTimeA = new Date(a.update_time).getTime();
|
||||||
|
const updateTimeB = new Date(b.update_time).getTime();
|
||||||
|
|
||||||
|
return updateTimeB - updateTimeA;
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatDateTime = (dateTimeStr: string) => {
|
||||||
|
try {
|
||||||
|
const date = new Date(dateTimeStr);
|
||||||
|
|
||||||
|
const day = date.getDate().toString().padStart(2, "0");
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const hours = date.getHours().toString().padStart(2, "0");
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, "0");
|
||||||
|
|
||||||
|
return `${day}/${month}/${year} ${hours}:${minutes}`;
|
||||||
|
} catch (error) {
|
||||||
|
return "Invalid date";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getExternalId = (sessionId: string) => {
|
||||||
|
return sessionId.split("_")[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-64 border-r border-neutral-700 flex flex-col bg-neutral-900">
|
||||||
|
<div className="p-4 border-b border-neutral-700">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsNewChatDialogOpen(true)}
|
||||||
|
className="bg-emerald-800 text-emerald-100 hover:bg-emerald-700 border-emerald-700"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-1" /> New Conversation
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-neutral-500" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search conversations..."
|
||||||
|
className="pl-9 bg-neutral-800 border-neutral-700 text-neutral-200 focus-visible:ring-emerald-500"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-neutral-400 hover:text-white hover:bg-neutral-800"
|
||||||
|
onClick={() => setShowAgentFilter(!showAgentFilter)}
|
||||||
|
>
|
||||||
|
<Filter className="h-4 w-4 mr-1" />
|
||||||
|
Filter
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{selectedAgentFilter !== "all" && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setSelectedAgentFilter("all")}
|
||||||
|
className="text-neutral-400 hover:text-white hover:bg-neutral-800"
|
||||||
|
>
|
||||||
|
Clear filter
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showAgentFilter && (
|
||||||
|
<div className="pt-1">
|
||||||
|
<Select
|
||||||
|
value={selectedAgentFilter}
|
||||||
|
onValueChange={setSelectedAgentFilter}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||||
|
<SelectValue placeholder="Filter by agent" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-neutral-900 border-neutral-700 text-white">
|
||||||
|
<SelectItem
|
||||||
|
value="all"
|
||||||
|
className="data-[selected]:bg-neutral-800 data-[highlighted]:bg-neutral-800 !text-white hover:text-emerald-400 data-[selected]:!text-emerald-400"
|
||||||
|
>
|
||||||
|
All agents
|
||||||
|
</SelectItem>
|
||||||
|
{agents.map((agent) => (
|
||||||
|
<SelectItem
|
||||||
|
key={agent.id}
|
||||||
|
value={agent.id}
|
||||||
|
className="data-[selected]:bg-neutral-800 data-[highlighted]:bg-neutral-800 !text-white hover:text-emerald-400 data-[selected]:!text-emerald-400"
|
||||||
|
>
|
||||||
|
{agent.name.slice(0, 15)}{" "}
|
||||||
|
{agent.name.length > 15 && "..."}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex justify-center items-center h-24">
|
||||||
|
<Loader2 className="h-5 w-5 text-emerald-400 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : sortedSessions.length > 0 ? (
|
||||||
|
<div className="px-4 pt-2 space-y-2">
|
||||||
|
{sortedSessions.map((session) => {
|
||||||
|
const agentId = session.id.split("_")[1];
|
||||||
|
const agentInfo = agents.find((a) => a.id === agentId);
|
||||||
|
const externalId = getExternalId(session.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={session.id}
|
||||||
|
className={`p-3 rounded-md cursor-pointer transition-colors group relative ${
|
||||||
|
selectedSession === session.id
|
||||||
|
? "bg-emerald-800/20 border border-emerald-600/40"
|
||||||
|
: "bg-neutral-800 hover:bg-neutral-700 border border-transparent"
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedSession(session.id)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-emerald-500 mr-2"></div>
|
||||||
|
<div className="text-neutral-200 font-medium truncate max-w-[180px]">
|
||||||
|
{externalId}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 flex items-center gap-2">
|
||||||
|
{agentInfo && (
|
||||||
|
<Badge className="bg-neutral-700 text-emerald-400 border-neutral-600 text-xs">
|
||||||
|
{agentInfo.name.slice(0, 15)}
|
||||||
|
{agentInfo.name.length > 15 && "..."}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
<div className="text-xs text-neutral-500 ml-auto">
|
||||||
|
{formatDateTime(session.update_time)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : searchTerm || selectedAgentFilter !== "all" ? (
|
||||||
|
<div className="text-center py-4 text-neutral-400">
|
||||||
|
No results found
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-4 text-neutral-400">
|
||||||
|
Click "New" to start
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
881
frontend/app/chat/page.tsx
Normal file
881
frontend/app/chat/page.tsx
Normal file
@@ -0,0 +1,881 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/chat/page.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
|
import {
|
||||||
|
MessageSquare,
|
||||||
|
Send,
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
|
Loader2,
|
||||||
|
X,
|
||||||
|
Trash2,
|
||||||
|
Bot,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
DialogHeader,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { listAgents } from "@/services/agentService";
|
||||||
|
import {
|
||||||
|
listSessions,
|
||||||
|
getSessionMessages,
|
||||||
|
ChatMessage,
|
||||||
|
deleteSession,
|
||||||
|
ChatSession,
|
||||||
|
ChatPart
|
||||||
|
} from "@/services/sessionService";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { useAgentWebSocket } from "@/hooks/use-agent-webSocket";
|
||||||
|
import { getAccessTokenFromCookie } from "@/lib/utils";
|
||||||
|
import { ChatMessage as ChatMessageComponent } from "./components/ChatMessage";
|
||||||
|
import { SessionList } from "./components/SessionList";
|
||||||
|
import { ChatInput } from "./components/ChatInput";
|
||||||
|
import { FileData } from "@/lib/file-utils";
|
||||||
|
import { AgentInfoDialog } from "./components/AgentInfoDialog";
|
||||||
|
|
||||||
|
interface FunctionMessageContent {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
author?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Chat() {
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [agents, setAgents] = useState<any[]>([]);
|
||||||
|
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||||
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [agentSearchTerm, setAgentSearchTerm] = useState("");
|
||||||
|
const [selectedAgentFilter, setSelectedAgentFilter] = useState<string>("all");
|
||||||
|
const [messageInput, setMessageInput] = useState("");
|
||||||
|
const [selectedSession, setSelectedSession] = useState<string | null>(null);
|
||||||
|
const [currentAgentId, setCurrentAgentId] = useState<string | null>(null);
|
||||||
|
const [isSending, setIsSending] = useState(false);
|
||||||
|
const [isNewChatDialogOpen, setIsNewChatDialogOpen] = useState(false);
|
||||||
|
const [showAgentFilter, setShowAgentFilter] = useState(false);
|
||||||
|
const [expandedFunctions, setExpandedFunctions] = useState<
|
||||||
|
Record<string, boolean>
|
||||||
|
>({});
|
||||||
|
const [isAgentInfoDialogOpen, setIsAgentInfoDialogOpen] = useState(false);
|
||||||
|
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const user =
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? JSON.parse(localStorage.getItem("user") || "{}")
|
||||||
|
: {};
|
||||||
|
const clientId = user?.client_id || "test";
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
if (messagesContainerRef.current) {
|
||||||
|
messagesContainerRef.current.scrollTop =
|
||||||
|
messagesContainerRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadData = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const agentsResponse = await listAgents(clientId);
|
||||||
|
setAgents(agentsResponse.data);
|
||||||
|
|
||||||
|
const sessionsResponse = await listSessions(clientId);
|
||||||
|
setSessions(sessionsResponse.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading data:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
}, [clientId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedSession) {
|
||||||
|
setMessages([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadMessages = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const response = await getSessionMessages(selectedSession);
|
||||||
|
setMessages(response.data);
|
||||||
|
|
||||||
|
const agentId = selectedSession.split("_")[1];
|
||||||
|
setCurrentAgentId(agentId);
|
||||||
|
|
||||||
|
setTimeout(scrollToBottom, 100);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading messages:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadMessages();
|
||||||
|
}, [selectedSession]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (messages.length > 0) {
|
||||||
|
setTimeout(scrollToBottom, 100);
|
||||||
|
}
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
const filteredSessions = sessions.filter((session) => {
|
||||||
|
const matchesSearchTerm = session.id
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchTerm.toLowerCase());
|
||||||
|
|
||||||
|
if (selectedAgentFilter === "all") {
|
||||||
|
return matchesSearchTerm;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionAgentId = session.id.split("_")[1];
|
||||||
|
return matchesSearchTerm && sessionAgentId === selectedAgentFilter;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedSessions = [...filteredSessions].sort((a, b) => {
|
||||||
|
const updateTimeA = new Date(a.update_time).getTime();
|
||||||
|
const updateTimeB = new Date(b.update_time).getTime();
|
||||||
|
|
||||||
|
return updateTimeB - updateTimeA;
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatDateTime = (dateTimeStr: string) => {
|
||||||
|
try {
|
||||||
|
const date = new Date(dateTimeStr);
|
||||||
|
|
||||||
|
const day = date.getDate().toString().padStart(2, "0");
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const hours = date.getHours().toString().padStart(2, "0");
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, "0");
|
||||||
|
|
||||||
|
return `${day}/${month}/${year} ${hours}:${minutes}`;
|
||||||
|
} catch (error) {
|
||||||
|
return "Invalid date";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredAgents = agents.filter(
|
||||||
|
(agent) =>
|
||||||
|
agent.name.toLowerCase().includes(agentSearchTerm.toLowerCase()) ||
|
||||||
|
(agent.description &&
|
||||||
|
agent.description.toLowerCase().includes(agentSearchTerm.toLowerCase()))
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectAgent = (agentId: string) => {
|
||||||
|
setCurrentAgentId(agentId);
|
||||||
|
setSelectedSession(null);
|
||||||
|
setMessages([]);
|
||||||
|
setIsNewChatDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendMessage = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!messageInput.trim() || !currentAgentId) return;
|
||||||
|
setIsSending(true);
|
||||||
|
setMessages((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
id: `temp-${Date.now()}`,
|
||||||
|
content: {
|
||||||
|
parts: [{ text: messageInput }],
|
||||||
|
role: "user",
|
||||||
|
},
|
||||||
|
author: "user",
|
||||||
|
timestamp: Date.now() / 1000,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
wsSendMessage(messageInput);
|
||||||
|
setMessageInput("");
|
||||||
|
const textarea = document.querySelector("textarea");
|
||||||
|
if (textarea) textarea.style.height = "auto";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendMessageWithFiles = (message: string, files?: FileData[]) => {
|
||||||
|
if ((!message.trim() && (!files || files.length === 0)) || !currentAgentId)
|
||||||
|
return;
|
||||||
|
setIsSending(true);
|
||||||
|
|
||||||
|
const messageParts: ChatPart[] = [];
|
||||||
|
|
||||||
|
if (message.trim()) {
|
||||||
|
messageParts.push({ text: message });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
files.forEach(file => {
|
||||||
|
messageParts.push({
|
||||||
|
inline_data: {
|
||||||
|
data: file.data,
|
||||||
|
mime_type: file.content_type,
|
||||||
|
metadata: {
|
||||||
|
filename: file.filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessages((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
id: `temp-${Date.now()}`,
|
||||||
|
content: {
|
||||||
|
parts: messageParts,
|
||||||
|
role: "user"
|
||||||
|
},
|
||||||
|
author: "user",
|
||||||
|
timestamp: Date.now() / 1000,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
wsSendMessage(message, files);
|
||||||
|
|
||||||
|
setMessageInput("");
|
||||||
|
const textarea = document.querySelector("textarea");
|
||||||
|
if (textarea) textarea.style.height = "auto";
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateExternalId = () => {
|
||||||
|
const now = new Date();
|
||||||
|
return (
|
||||||
|
now.getFullYear().toString() +
|
||||||
|
(now.getMonth() + 1).toString().padStart(2, "0") +
|
||||||
|
now.getDate().toString().padStart(2, "0") +
|
||||||
|
now.getHours().toString().padStart(2, "0") +
|
||||||
|
now.getMinutes().toString().padStart(2, "0") +
|
||||||
|
now.getSeconds().toString().padStart(2, "0") +
|
||||||
|
now.getMilliseconds().toString().padStart(3, "0")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentAgent = agents.find((agent) => agent.id === currentAgentId);
|
||||||
|
|
||||||
|
const getCurrentSessionInfo = () => {
|
||||||
|
if (!selectedSession) return null;
|
||||||
|
|
||||||
|
const parts = selectedSession.split("_");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dateStr = parts[0];
|
||||||
|
if (dateStr.length >= 8) {
|
||||||
|
const year = dateStr.substring(0, 4);
|
||||||
|
const month = dateStr.substring(4, 6);
|
||||||
|
const day = dateStr.substring(6, 8);
|
||||||
|
|
||||||
|
return {
|
||||||
|
externalId: parts[0],
|
||||||
|
agentId: parts[1],
|
||||||
|
displayDate: `${day}/${month}/${year}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error processing session ID:", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
externalId: parts[0],
|
||||||
|
agentId: parts[1],
|
||||||
|
displayDate: "Session",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getExternalId = (sessionId: string) => {
|
||||||
|
return sessionId.split("_")[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const containsMarkdown = (text: string): boolean => {
|
||||||
|
if (!text || text.length < 3) return false;
|
||||||
|
|
||||||
|
const markdownPatterns = [
|
||||||
|
/[*_]{1,2}[^*_]+[*_]{1,2}/, // bold/italic
|
||||||
|
/\[[^\]]+\]\([^)]+\)/, // links
|
||||||
|
/^#{1,6}\s/m, // headers
|
||||||
|
/^[-*+]\s/m, // unordered lists
|
||||||
|
/^[0-9]+\.\s/m, // ordered lists
|
||||||
|
/^>\s/m, // block quotes
|
||||||
|
/`[^`]+`/, // inline code
|
||||||
|
/```[\s\S]*?```/, // code blocks
|
||||||
|
/^\|(.+\|)+$/m, // tables
|
||||||
|
/!\[[^\]]*\]\([^)]+\)/, // images
|
||||||
|
];
|
||||||
|
|
||||||
|
return markdownPatterns.some((pattern) => pattern.test(text));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMessageText = (
|
||||||
|
message: ChatMessage
|
||||||
|
): string | FunctionMessageContent => {
|
||||||
|
const author = message.author;
|
||||||
|
const parts = message.content.parts;
|
||||||
|
|
||||||
|
if (!parts || parts.length === 0) return "Empty content";
|
||||||
|
|
||||||
|
const functionCallPart = parts.find(
|
||||||
|
(part) => part.functionCall || part.function_call
|
||||||
|
);
|
||||||
|
const functionResponsePart = parts.find(
|
||||||
|
(part) => part.functionResponse || part.function_response
|
||||||
|
);
|
||||||
|
|
||||||
|
const inlineDataParts = parts.filter((part) => part.inline_data);
|
||||||
|
|
||||||
|
if (functionCallPart) {
|
||||||
|
const funcCall =
|
||||||
|
functionCallPart.functionCall || functionCallPart.function_call || {};
|
||||||
|
const args = funcCall.args || {};
|
||||||
|
const name = funcCall.name || "unknown";
|
||||||
|
const id = funcCall.id || "no-id";
|
||||||
|
|
||||||
|
return {
|
||||||
|
author,
|
||||||
|
title: `📞 Function call: ${name}`,
|
||||||
|
content: `ID: ${id}
|
||||||
|
Args: ${
|
||||||
|
Object.keys(args).length > 0
|
||||||
|
? `\n${JSON.stringify(args, null, 2)}`
|
||||||
|
: "{}"
|
||||||
|
}`,
|
||||||
|
} as FunctionMessageContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (functionResponsePart) {
|
||||||
|
const funcResponse =
|
||||||
|
functionResponsePart.functionResponse ||
|
||||||
|
functionResponsePart.function_response ||
|
||||||
|
{};
|
||||||
|
const response = funcResponse.response || {};
|
||||||
|
const name = funcResponse.name || "unknown";
|
||||||
|
const id = funcResponse.id || "no-id";
|
||||||
|
const status = response.status || "unknown";
|
||||||
|
const statusEmoji = status === "error" ? "❌" : "✅";
|
||||||
|
|
||||||
|
let resultText = "";
|
||||||
|
if (status === "error") {
|
||||||
|
resultText = `Error: ${response.error_message || "Unknown error"}`;
|
||||||
|
} else if (response.report) {
|
||||||
|
resultText = `Result: ${response.report}`;
|
||||||
|
} else if (response.result && response.result.content) {
|
||||||
|
const content = response.result.content;
|
||||||
|
if (Array.isArray(content) && content.length > 0 && content[0].text) {
|
||||||
|
try {
|
||||||
|
const textContent = content[0].text;
|
||||||
|
const parsedJson = JSON.parse(textContent);
|
||||||
|
resultText = `Result: \n${JSON.stringify(parsedJson, null, 2)}`;
|
||||||
|
} catch (e) {
|
||||||
|
resultText = `Result: ${content[0].text}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resultText = `Result:\n${JSON.stringify(response, null, 2)}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resultText = `Result:\n${JSON.stringify(response, null, 2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
author,
|
||||||
|
title: `${statusEmoji} Function response: ${name}`,
|
||||||
|
content: `ID: ${id}\n${resultText}`,
|
||||||
|
} as FunctionMessageContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length === 1 && parts[0].text) {
|
||||||
|
return {
|
||||||
|
author,
|
||||||
|
content: parts[0].text,
|
||||||
|
title: "Message",
|
||||||
|
} as FunctionMessageContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textParts = parts
|
||||||
|
.filter((part) => part.text)
|
||||||
|
.map((part) => part.text)
|
||||||
|
.filter((text) => text);
|
||||||
|
|
||||||
|
if (textParts.length > 0) {
|
||||||
|
return {
|
||||||
|
author,
|
||||||
|
content: textParts.join("\n\n"),
|
||||||
|
title: "Message",
|
||||||
|
} as FunctionMessageContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Empty content";
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleFunctionExpansion = (messageId: string) => {
|
||||||
|
setExpandedFunctions((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[messageId]: !prev[messageId],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const agentColors: Record<string, string> = {
|
||||||
|
Assistant: "bg-emerald-400",
|
||||||
|
Programmer: "bg-[#00cc7d]",
|
||||||
|
Writer: "bg-[#00b8ff]",
|
||||||
|
Researcher: "bg-[#ff9d00]",
|
||||||
|
Planner: "bg-[#9d00ff]",
|
||||||
|
default: "bg-[#333]",
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAgentColor = (agentName: string) => {
|
||||||
|
return agentColors[agentName] || agentColors.default;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSendMessage(e as unknown as React.FormEvent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const autoResizeTextarea = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const textarea = e.target;
|
||||||
|
|
||||||
|
textarea.style.height = "auto";
|
||||||
|
|
||||||
|
const maxHeight = 10 * 24;
|
||||||
|
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
|
||||||
|
|
||||||
|
textarea.style.height = `${newHeight}px`;
|
||||||
|
|
||||||
|
setMessageInput(textarea.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteSession = async () => {
|
||||||
|
if (!selectedSession) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteSession(selectedSession);
|
||||||
|
|
||||||
|
setSessions(sessions.filter((session) => session.id !== selectedSession));
|
||||||
|
setSelectedSession(null);
|
||||||
|
setMessages([]);
|
||||||
|
setCurrentAgentId(null);
|
||||||
|
setIsDeleteDialogOpen(false);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Session deleted successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting session:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error deleting session",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEvent = useCallback((event: any) => {
|
||||||
|
setMessages((prev) => [...prev, event]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onTurnComplete = useCallback(() => {
|
||||||
|
setIsSending(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAgentInfoClick = () => {
|
||||||
|
setIsAgentInfoDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAgentUpdated = (updatedAgent: any) => {
|
||||||
|
setAgents(agents.map(agent =>
|
||||||
|
agent.id === updatedAgent.id ? updatedAgent : agent
|
||||||
|
));
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Agent updated successfully",
|
||||||
|
description: "The agent has been updated with the new settings.",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const jwt = getAccessTokenFromCookie();
|
||||||
|
|
||||||
|
const agentId = useMemo(() => currentAgentId || "", [currentAgentId]);
|
||||||
|
const externalId = useMemo(
|
||||||
|
() =>
|
||||||
|
selectedSession ? getExternalId(selectedSession) : generateExternalId(),
|
||||||
|
[selectedSession]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { sendMessage: wsSendMessage, disconnect: _ } = useAgentWebSocket({
|
||||||
|
agentId,
|
||||||
|
externalId,
|
||||||
|
jwt,
|
||||||
|
onEvent,
|
||||||
|
onTurnComplete,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen max-h-screen bg-[#121212]">
|
||||||
|
<SessionList
|
||||||
|
sessions={sessions}
|
||||||
|
agents={agents}
|
||||||
|
selectedSession={selectedSession}
|
||||||
|
isLoading={isLoading}
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
selectedAgentFilter={selectedAgentFilter}
|
||||||
|
showAgentFilter={showAgentFilter}
|
||||||
|
setSearchTerm={setSearchTerm}
|
||||||
|
setSelectedAgentFilter={setSelectedAgentFilter}
|
||||||
|
setShowAgentFilter={setShowAgentFilter}
|
||||||
|
setSelectedSession={setSelectedSession}
|
||||||
|
setIsNewChatDialogOpen={setIsNewChatDialogOpen}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
|
{selectedSession || currentAgentId ? (
|
||||||
|
<>
|
||||||
|
<div className="p-4 border-b border-[#333] bg-neutral-900 shadow-md">
|
||||||
|
{(() => {
|
||||||
|
const sessionInfo = getCurrentSessionInfo();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h2 className="text-xl font-bold text-white flex items-center gap-2">
|
||||||
|
<div className="p-1 rounded-full bg-emerald-500/20">
|
||||||
|
<MessageSquare className="h-5 w-5 text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
{selectedSession
|
||||||
|
? `Session ${
|
||||||
|
sessionInfo?.externalId || selectedSession
|
||||||
|
}`
|
||||||
|
: "New Conversation"}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{currentAgent && (
|
||||||
|
<Badge
|
||||||
|
className="bg-emerald-500 text-white px-3 py-1 text-sm border-0 cursor-pointer hover:bg-emerald-600 transition-colors"
|
||||||
|
onClick={handleAgentInfoClick}
|
||||||
|
>
|
||||||
|
{currentAgent.name || currentAgentId}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedSession && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 text-neutral-400 hover:text-red-500 hover:bg-[#333]"
|
||||||
|
onClick={() => setIsDeleteDialogOpen(true)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={messagesContainerRef}
|
||||||
|
className="flex-1 overflow-y-auto p-4 bg-neutral-950"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full">
|
||||||
|
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-emerald-500 to-emerald-700 flex items-center justify-center shadow-lg mb-4 relative">
|
||||||
|
<Loader2 className="h-6 w-6 text-white animate-spin" />
|
||||||
|
<div className="absolute inset-0 rounded-full blur-md bg-emerald-400/20 animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
<p className="text-neutral-400 mb-2">Loading conversation...</p>
|
||||||
|
</div>
|
||||||
|
) : messages.length === 0 ? (
|
||||||
|
<div className="flex flex-col h-full items-center justify-center text-center p-6">
|
||||||
|
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-emerald-500/20 to-emerald-700/20 flex items-center justify-center shadow-lg mb-5 border border-emerald-500/30">
|
||||||
|
<MessageSquare className="h-6 w-6 text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium text-neutral-300 mb-2">
|
||||||
|
{currentAgent ? `Chat with ${currentAgent.name}` : "New Conversation"}
|
||||||
|
</h3>
|
||||||
|
<p className="text-neutral-500 text-sm max-w-md">
|
||||||
|
Type your message below to start the conversation. This chat will help you interact with the agent and explore its capabilities.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4 w-full max-w-full">
|
||||||
|
{messages.map((message) => {
|
||||||
|
const messageContent = getMessageText(message);
|
||||||
|
const agentColor = getAgentColor(message.author);
|
||||||
|
const isExpanded = expandedFunctions[message.id] || false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChatMessageComponent
|
||||||
|
key={message.id}
|
||||||
|
message={message}
|
||||||
|
agentColor={agentColor}
|
||||||
|
isExpanded={isExpanded}
|
||||||
|
toggleExpansion={toggleFunctionExpansion}
|
||||||
|
containsMarkdown={containsMarkdown}
|
||||||
|
messageContent={messageContent}
|
||||||
|
sessionId={selectedSession as string}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{isSending && (
|
||||||
|
<div className="flex justify-start">
|
||||||
|
<div className="flex gap-3 max-w-[80%]">
|
||||||
|
<Avatar
|
||||||
|
className="bg-gradient-to-br from-purple-600 to-purple-800 shadow-md border-0"
|
||||||
|
>
|
||||||
|
<AvatarFallback className="bg-transparent">
|
||||||
|
<Bot className="h-4 w-4 text-white" />
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="rounded-lg p-3 bg-gradient-to-br from-neutral-800 to-neutral-900 border border-neutral-700/50 shadow-md">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<div className="h-2 w-2 rounded-full bg-emerald-400 animate-bounce"></div>
|
||||||
|
<div className="h-2 w-2 rounded-full bg-emerald-400 animate-bounce [animation-delay:0.2s]"></div>
|
||||||
|
<div className="h-2 w-2 rounded-full bg-emerald-400 animate-bounce [animation-delay:0.4s]"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 pt-4 pb-6 border-t border-[#333] bg-neutral-900 shadow-inner">
|
||||||
|
{isSending && !isLoading && (
|
||||||
|
<div className="px-4 py-2 mb-3 rounded-lg bg-neutral-800/50 border border-neutral-700/30 text-sm text-neutral-400 flex items-center shadow-sm">
|
||||||
|
<div className="mr-2 relative">
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin text-emerald-400" />
|
||||||
|
<div className="absolute inset-0 blur-sm bg-emerald-400/20 rounded-full animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
Agent is thinking...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="rounded-lg shadow-md bg-neutral-800/20 border border-neutral-700/30 p-1">
|
||||||
|
<ChatInput
|
||||||
|
onSendMessage={handleSendMessageWithFiles}
|
||||||
|
isLoading={isSending || isLoading}
|
||||||
|
placeholder="Type your message..."
|
||||||
|
autoFocus={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||||
|
<div className="w-20 h-20 rounded-full bg-emerald-500/20 flex items-center justify-center shadow-lg mb-6 border border-emerald-500/30">
|
||||||
|
<MessageSquare className="h-10 w-10 text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-semibold text-white mb-3">
|
||||||
|
Select a conversation
|
||||||
|
</h2>
|
||||||
|
<p className="text-neutral-400 mb-8 max-w-md">
|
||||||
|
Choose an existing conversation or start a new one.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsNewChatDialogOpen(true)}
|
||||||
|
className="bg-emerald-500 text-white hover:bg-emerald-600 px-6 py-6 h-auto shadow-md rounded-xl"
|
||||||
|
>
|
||||||
|
<Plus className="mr-2 h-5 w-5" />
|
||||||
|
New Conversation
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog open={isNewChatDialogOpen} onOpenChange={setIsNewChatDialogOpen}>
|
||||||
|
<DialogContent className="bg-neutral-900 border-neutral-800 text-white shadow-xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<div className="p-1.5 rounded-full bg-emerald-500/20">
|
||||||
|
<MessageSquare className="h-5 w-5 text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
<DialogTitle className="text-xl font-medium text-white">
|
||||||
|
New Conversation
|
||||||
|
</DialogTitle>
|
||||||
|
</div>
|
||||||
|
<DialogDescription className="text-neutral-400">
|
||||||
|
Select an agent to start a new conversation.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="mt-4 space-y-4">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-500">
|
||||||
|
<Search className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder="Search agents..."
|
||||||
|
className="pl-10 bg-neutral-800/40 border-neutral-700/50 text-white focus-visible:ring-emerald-500/50 focus-visible:border-emerald-500/50 shadow-inner rounded-xl"
|
||||||
|
value={agentSearchTerm}
|
||||||
|
onChange={(e) => setAgentSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
{agentSearchTerm && (
|
||||||
|
<button
|
||||||
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-neutral-400 hover:text-emerald-500 transition-colors"
|
||||||
|
onClick={() => setAgentSearchTerm("")}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm text-neutral-300 mb-2">Choose an agent:</div>
|
||||||
|
|
||||||
|
<ScrollArea className="h-[300px] pr-2">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-8">
|
||||||
|
<div className="relative">
|
||||||
|
<Loader2 className="h-8 w-8 text-emerald-400 animate-spin" />
|
||||||
|
<div className="absolute inset-0 rounded-full blur-md bg-emerald-400/20 animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
<p className="text-neutral-400 mt-4">Loading agents...</p>
|
||||||
|
</div>
|
||||||
|
) : filteredAgents.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{filteredAgents.map((agent) => (
|
||||||
|
<div
|
||||||
|
key={agent.id}
|
||||||
|
className="p-3 rounded-md cursor-pointer transition-all bg-neutral-800 hover:bg-neutral-800/90 border border-neutral-700/30 hover:border-emerald-500/30 shadow-sm hover:shadow-md group"
|
||||||
|
onClick={() => selectAgent(agent.id)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<div className="p-1 rounded-full bg-emerald-500/20 group-hover:bg-emerald-500/30 transition-colors">
|
||||||
|
<MessageSquare size={14} className="text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
<span className="font-medium text-white group-hover:text-emerald-50">
|
||||||
|
{agent.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between mt-1">
|
||||||
|
<Badge className="text-xs bg-neutral-800/60 text-emerald-400 border border-emerald-500/30">
|
||||||
|
{agent.type}
|
||||||
|
</Badge>
|
||||||
|
{agent.model && (
|
||||||
|
<span className="text-xs text-neutral-400">
|
||||||
|
{agent.model}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{agent.description && (
|
||||||
|
<p className="text-xs text-neutral-300 mt-2 line-clamp-2 group-hover:text-neutral-200">
|
||||||
|
{agent.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : agentSearchTerm ? (
|
||||||
|
<div className="text-center py-4 text-neutral-400">
|
||||||
|
No agent found with "{agentSearchTerm}"
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-4 text-neutral-400">
|
||||||
|
<p>No agents available</p>
|
||||||
|
<p className="text-sm mt-2">
|
||||||
|
Create agents in the Agent Management screen
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsNewChatDialogOpen(false)}
|
||||||
|
variant="outline"
|
||||||
|
className="bg-neutral-800/40 border-neutral-700/50 text-neutral-300 hover:bg-neutral-700/50 hover:text-white hover:border-neutral-600"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||||
|
<DialogContent className="bg-neutral-900 border-neutral-800 text-white shadow-xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<div className="p-1.5 rounded-full bg-red-500/20">
|
||||||
|
<Trash2 className="h-5 w-5 text-red-400" />
|
||||||
|
</div>
|
||||||
|
<DialogTitle className="text-xl font-medium text-white">
|
||||||
|
Delete Session
|
||||||
|
</DialogTitle>
|
||||||
|
</div>
|
||||||
|
<DialogDescription className="text-neutral-400">
|
||||||
|
Are you sure you want to delete this session? This action cannot
|
||||||
|
be undone.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<DialogFooter className="gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsDeleteDialogOpen(false)}
|
||||||
|
variant="outline"
|
||||||
|
className="bg-neutral-800/40 border-neutral-700/50 text-neutral-300 hover:bg-neutral-700/50 hover:text-white hover:border-neutral-600"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleDeleteSession}
|
||||||
|
className="bg-red-600 hover:bg-red-700 text-white border-0 shadow-md"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Agent Info Dialog */}
|
||||||
|
<AgentInfoDialog
|
||||||
|
agent={currentAgent}
|
||||||
|
open={isAgentInfoDialogOpen}
|
||||||
|
onOpenChange={setIsAgentInfoDialogOpen}
|
||||||
|
onAgentUpdated={handleAgentUpdated}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
52
frontend/app/client-layout.tsx
Normal file
52
frontend/app/client-layout.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/client-layout.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import type React from "react"
|
||||||
|
import { usePathname } from "next/navigation"
|
||||||
|
import Sidebar from "@/components/sidebar"
|
||||||
|
|
||||||
|
export default function ClientLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
const pathname = usePathname()
|
||||||
|
const isLoginPage = pathname === "/login"
|
||||||
|
const isVerifyEmailPage = pathname.startsWith("/security/verify-email")
|
||||||
|
const isResetPasswordPage = pathname.startsWith("/security/reset-password")
|
||||||
|
const isSharedChatPage = pathname.startsWith("/shared-chat")
|
||||||
|
|
||||||
|
if (isLoginPage || isVerifyEmailPage || isResetPasswordPage || isSharedChatPage) {
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen bg-[#121212]">
|
||||||
|
<Sidebar />
|
||||||
|
<main className="flex-1 overflow-auto">{children}</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
31
frontend/app/clients/loading.tsx
Normal file
31
frontend/app/clients/loading.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/clients/loading.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
export default function Loading() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
462
frontend/app/clients/page.tsx
Normal file
462
frontend/app/clients/page.tsx
Normal file
@@ -0,0 +1,462 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/clients/page.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import type React from "react"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { useToast } from "@/components/ui/use-toast"
|
||||||
|
import { Plus, MoreHorizontal, Edit, Trash2, Search, Users, UserPlus } from "lucide-react"
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@/components/ui/alert-dialog"
|
||||||
|
import {
|
||||||
|
createClient,
|
||||||
|
listClients,
|
||||||
|
getClient,
|
||||||
|
updateClient,
|
||||||
|
deleteClient,
|
||||||
|
impersonateClient,
|
||||||
|
Client,
|
||||||
|
} from "@/services/clientService"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
|
||||||
|
export default function ClientsPage() {
|
||||||
|
const { toast } = useToast()
|
||||||
|
const router = useRouter()
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [searchQuery, setSearchQuery] = useState("")
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||||
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
|
||||||
|
const [selectedClient, setSelectedClient] = useState<Client | null>(null)
|
||||||
|
|
||||||
|
const [clientData, setClientData] = useState({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [limit, setLimit] = useState(1000)
|
||||||
|
const [total, setTotal] = useState(0)
|
||||||
|
|
||||||
|
const [clients, setClients] = useState<Client[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchClients = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await listClients((page - 1) * limit, limit)
|
||||||
|
setClients(res.data)
|
||||||
|
setTotal(res.data.length)
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error loading clients",
|
||||||
|
description: "Unable to load clients.",
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchClients()
|
||||||
|
}, [page, limit])
|
||||||
|
|
||||||
|
const filteredClients = Array.isArray(clients)
|
||||||
|
? clients.filter(
|
||||||
|
(client) =>
|
||||||
|
client.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
client.email.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
|
||||||
|
const handleAddClient = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
if (selectedClient) {
|
||||||
|
await updateClient(selectedClient.id, clientData)
|
||||||
|
toast({
|
||||||
|
title: "Client updated",
|
||||||
|
description: `${clientData.name} was updated successfully.`,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await createClient({ ...clientData, password: "Password@123" })
|
||||||
|
toast({
|
||||||
|
title: "Client added",
|
||||||
|
description: `${clientData.name} was added successfully.`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setIsDialogOpen(false)
|
||||||
|
resetForm()
|
||||||
|
const res = await listClients((page - 1) * limit, limit)
|
||||||
|
setClients(res.data)
|
||||||
|
setTotal(res.data.length)
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Unable to save client. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditClient = async (client: Client) => {
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await getClient(client.id)
|
||||||
|
setSelectedClient(res.data)
|
||||||
|
setClientData({
|
||||||
|
name: res.data.name,
|
||||||
|
email: res.data.email,
|
||||||
|
})
|
||||||
|
setIsDialogOpen(true)
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error searching client",
|
||||||
|
description: "Unable to search client.",
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDeleteClient = async () => {
|
||||||
|
if (!selectedClient) return
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
await deleteClient(selectedClient.id)
|
||||||
|
toast({
|
||||||
|
title: "Client deleted",
|
||||||
|
description: `${selectedClient.name} was deleted successfully.`,
|
||||||
|
})
|
||||||
|
setIsDeleteDialogOpen(false)
|
||||||
|
setSelectedClient(null)
|
||||||
|
const res = await listClients((page - 1) * limit, limit)
|
||||||
|
setClients(res.data)
|
||||||
|
setTotal(res.data.length)
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Unable to delete client. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImpersonateClient = async (client: Client) => {
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
const response = await impersonateClient(client.id)
|
||||||
|
|
||||||
|
const currentUser = localStorage.getItem("user")
|
||||||
|
if (currentUser) {
|
||||||
|
localStorage.setItem("adminUser", currentUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentToken = document.cookie.match(/access_token=([^;]+)/)?.[1]
|
||||||
|
if (currentToken) {
|
||||||
|
localStorage.setItem("adminToken", currentToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem("isImpersonating", "true")
|
||||||
|
localStorage.setItem("impersonatedClient", client.name)
|
||||||
|
|
||||||
|
document.cookie = `isImpersonating=true; path=/; max-age=${60 * 60 * 24 * 7}`
|
||||||
|
document.cookie = `impersonatedClient=${encodeURIComponent(client.name)}; path=/; max-age=${60 * 60 * 24 * 7}`
|
||||||
|
document.cookie = `access_token=${response.access_token}; path=/; max-age=${60 * 60 * 24 * 7}`
|
||||||
|
|
||||||
|
const userData = {
|
||||||
|
...JSON.parse(localStorage.getItem("user") || "{}"),
|
||||||
|
is_admin: false,
|
||||||
|
client_id: client.id
|
||||||
|
}
|
||||||
|
localStorage.setItem("user", JSON.stringify(userData))
|
||||||
|
document.cookie = `user=${encodeURIComponent(JSON.stringify(userData))}; path=/; max-age=${60 * 60 * 24 * 7}`
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Impersonation mode activated",
|
||||||
|
description: `You are viewing as ${client.name}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
router.push("/agents")
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error impersonating client:", error)
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Unable to impersonate client",
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setClientData({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
})
|
||||||
|
setSelectedClient(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-3xl font-bold text-white">Client Management</h1>
|
||||||
|
|
||||||
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button onClick={resetForm} className="bg-emerald-400 text-black hover:bg-[#00cc7d]">
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
New Client
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[500px] bg-[#1a1a1a] border-[#333]">
|
||||||
|
<form onSubmit={handleAddClient}>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-white">{selectedClient ? "Edit Client" : "New Client"}</DialogTitle>
|
||||||
|
<DialogDescription className="text-neutral-400">
|
||||||
|
{selectedClient
|
||||||
|
? "Edit the existing client information."
|
||||||
|
: "Fill in the information to create a new client."}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name" className="text-neutral-300">
|
||||||
|
Name
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={clientData.name}
|
||||||
|
onChange={(e) => setClientData({ ...clientData, name: e.target.value })}
|
||||||
|
className="bg-[#222] border-[#444] text-white"
|
||||||
|
placeholder="Company name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email" className="text-neutral-300">
|
||||||
|
Email
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={clientData.email}
|
||||||
|
onChange={(e) => setClientData({ ...clientData, email: e.target.value })}
|
||||||
|
className="bg-[#222] border-[#444] text-white"
|
||||||
|
placeholder="contact@company.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsDialogOpen(false)}
|
||||||
|
className="border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" className="bg-emerald-400 text-black hover:bg-[#00cc7d]" disabled={isLoading}>
|
||||||
|
{isLoading ? "Saving..." : selectedClient ? "Save Changes" : "Add Client"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||||
|
<AlertDialogContent className="bg-[#1a1a1a] border-[#333] text-white">
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Confirm delete</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription className="text-neutral-400">
|
||||||
|
Are you sure you want to delete the client "{selectedClient?.name}"? This action cannot be undone.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel className="border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white">
|
||||||
|
Cancel
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={confirmDeleteClient}
|
||||||
|
className="bg-red-600 text-white hover:bg-red-700"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? "Deleting..." : "Delete"}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="bg-[#1a1a1a] border-[#333] mb-6">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-white text-lg">Search Clients</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-neutral-500" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search by name or email..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="bg-[#222] border-[#444] text-white pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-[#1a1a1a] border-[#333]">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="border-[#333] hover:bg-[#222]">
|
||||||
|
<TableHead className="text-neutral-300">Name</TableHead>
|
||||||
|
<TableHead className="text-neutral-300">Email</TableHead>
|
||||||
|
<TableHead className="text-neutral-300">Created At</TableHead>
|
||||||
|
<TableHead className="text-neutral-300">Users</TableHead>
|
||||||
|
<TableHead className="text-neutral-300">Agents</TableHead>
|
||||||
|
<TableHead className="text-neutral-300 text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filteredClients.length > 0 ? (
|
||||||
|
filteredClients.map((client) => (
|
||||||
|
<TableRow key={client.id} className="border-[#333] hover:bg-[#222]">
|
||||||
|
<TableCell className="font-medium text-white">{client.name}</TableCell>
|
||||||
|
<TableCell className="text-neutral-300">{client.email}</TableCell>
|
||||||
|
<TableCell className="text-neutral-300">
|
||||||
|
{new Date(client.created_at).toLocaleDateString("pt-BR")}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-neutral-300">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Users className="h-4 w-4 mr-1 text-emerald-400" />
|
||||||
|
{client.users_count ?? 0}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-neutral-300">{client.agents_count ?? 0}</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0 text-neutral-300 hover:bg-[#333]">
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="bg-[#222] border-[#444] text-white">
|
||||||
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator className="bg-[#444]" />
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer hover:bg-[#333]"
|
||||||
|
onClick={() => handleEditClient(client)}
|
||||||
|
>
|
||||||
|
<Edit className="mr-2 h-4 w-4 text-emerald-400" />
|
||||||
|
Edit
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer hover:bg-[#333]"
|
||||||
|
onClick={() => handleImpersonateClient(client)}
|
||||||
|
>
|
||||||
|
<UserPlus className="mr-2 h-4 w-4 text-emerald-400" />
|
||||||
|
Enter as client
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer hover:bg-[#333] text-red-500"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedClient(client)
|
||||||
|
setIsDeleteDialogOpen(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={6} className="h-24 text-center text-neutral-500">
|
||||||
|
No clients found.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-4">
|
||||||
|
<Button onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page === 1 || isLoading}>
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
<span className="mx-4 text-white">Page {page} of {Math.ceil(total / limit) || 1}</span>
|
||||||
|
<Button onClick={() => setPage((p) => p + 1)} disabled={page * limit >= total || isLoading}>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
333
frontend/app/documentation/components/A2AComplianceCard.tsx
Normal file
333
frontend/app/documentation/components/A2AComplianceCard.tsx
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/documentation/components/A2AComplianceCard.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
import {
|
||||||
|
CheckCircle2,
|
||||||
|
Clock,
|
||||||
|
Shield,
|
||||||
|
Zap,
|
||||||
|
FileText,
|
||||||
|
Settings,
|
||||||
|
Network,
|
||||||
|
AlertCircle,
|
||||||
|
ExternalLink,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function A2AComplianceCard() {
|
||||||
|
const [showCoreFeatures, setShowCoreFeatures] = useState(false);
|
||||||
|
const [showAdvancedFeatures, setShowAdvancedFeatures] = useState(false);
|
||||||
|
|
||||||
|
const implementedFeatures = [
|
||||||
|
{
|
||||||
|
name: "JSON-RPC 2.0 Protocol",
|
||||||
|
status: "implemented",
|
||||||
|
icon: CheckCircle2,
|
||||||
|
description: "Full compliance with JSON-RPC 2.0 specification"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "message/send Method",
|
||||||
|
status: "implemented",
|
||||||
|
icon: CheckCircle2,
|
||||||
|
description: "Standard HTTP messaging with proper request/response format"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "message/stream Method",
|
||||||
|
status: "implemented",
|
||||||
|
icon: CheckCircle2,
|
||||||
|
description: "Real-time streaming via Server-Sent Events (SSE)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tasks/get Method",
|
||||||
|
status: "implemented",
|
||||||
|
icon: CheckCircle2,
|
||||||
|
description: "Task status querying and monitoring"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tasks/cancel Method",
|
||||||
|
status: "implemented",
|
||||||
|
icon: CheckCircle2,
|
||||||
|
description: "Task cancellation support"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "agent/authenticatedExtendedCard",
|
||||||
|
status: "implemented",
|
||||||
|
icon: CheckCircle2,
|
||||||
|
description: "Agent discovery and capability enumeration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "File Upload Support",
|
||||||
|
status: "implemented",
|
||||||
|
icon: CheckCircle2,
|
||||||
|
description: "Base64 file encoding with proper MIME type handling"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UUID v4 Message IDs",
|
||||||
|
status: "implemented",
|
||||||
|
icon: CheckCircle2,
|
||||||
|
description: "Standards-compliant unique message identification"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Authentication Methods",
|
||||||
|
status: "implemented",
|
||||||
|
icon: CheckCircle2,
|
||||||
|
description: "API Key and Bearer token authentication"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Task State Management",
|
||||||
|
status: "implemented",
|
||||||
|
icon: CheckCircle2,
|
||||||
|
description: "Complete task lifecycle: submitted → working → completed/failed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Artifact Handling",
|
||||||
|
status: "implemented",
|
||||||
|
icon: CheckCircle2,
|
||||||
|
description: "Complex response data with structured artifacts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CORS Compliance",
|
||||||
|
status: "implemented",
|
||||||
|
icon: CheckCircle2,
|
||||||
|
description: "Proper cross-origin resource sharing configuration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tasks/pushNotificationConfig/set",
|
||||||
|
status: "implemented",
|
||||||
|
icon: CheckCircle2,
|
||||||
|
description: "Set push notification configuration for tasks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tasks/pushNotificationConfig/get",
|
||||||
|
status: "implemented",
|
||||||
|
icon: CheckCircle2,
|
||||||
|
description: "Get push notification configuration for tasks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tasks/resubscribe",
|
||||||
|
status: "implemented",
|
||||||
|
icon: CheckCircle2,
|
||||||
|
description: "Resubscribe to task updates and notifications"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const advancedFeatures = [
|
||||||
|
{
|
||||||
|
name: "Push Notifications",
|
||||||
|
status: "implemented",
|
||||||
|
icon: CheckCircle2,
|
||||||
|
description: "A2A pushNotificationConfig methods and webhook support"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multi-turn Conversations",
|
||||||
|
status: "implemented",
|
||||||
|
icon: CheckCircle2,
|
||||||
|
description: "Context preservation via contextId field as per A2A specification"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Enhanced Error Diagnostics",
|
||||||
|
status: "implemented",
|
||||||
|
icon: AlertCircle,
|
||||||
|
description: "Comprehensive A2A error analysis and troubleshooting guidance"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const implementedCount = implementedFeatures.filter(f => f.status === 'implemented').length;
|
||||||
|
const totalFeatures = implementedFeatures.length + advancedFeatures.length;
|
||||||
|
const partialCount = advancedFeatures.filter(f => f.status === 'partial').length;
|
||||||
|
const advancedImplementedCount = advancedFeatures.filter(f => f.status === 'implemented').length;
|
||||||
|
const totalImplementedCount = implementedCount + advancedImplementedCount;
|
||||||
|
const completionPercentage = Math.round(((totalImplementedCount + (partialCount * 0.5)) / totalFeatures) * 100);
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'implemented': return 'text-green-400';
|
||||||
|
case 'partial': return 'text-yellow-400';
|
||||||
|
case 'planned': return 'text-blue-400';
|
||||||
|
default: return 'text-neutral-400';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusIcon = (status: string, IconComponent: any) => {
|
||||||
|
const colorClass = getStatusColor(status);
|
||||||
|
return <IconComponent className={`h-4 w-4 ${colorClass}`} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-gradient-to-br from-emerald-500/5 to-blue-500/5 border-emerald-500/20 text-white">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-emerald-400 flex items-center">
|
||||||
|
<Network className="h-5 w-5 mr-2" />
|
||||||
|
A2A Specification Compliance
|
||||||
|
</CardTitle>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex justify-between text-sm mb-1">
|
||||||
|
<span className="text-neutral-300">Implementation Progress</span>
|
||||||
|
<span className="text-emerald-400 font-medium">{completionPercentage}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress value={completionPercentage} className="h-2" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const shouldExpand = !showCoreFeatures || !showAdvancedFeatures;
|
||||||
|
setShowCoreFeatures(shouldExpand);
|
||||||
|
setShowAdvancedFeatures(shouldExpand);
|
||||||
|
}}
|
||||||
|
className="text-xs text-neutral-400 hover:text-white transition-colors px-2 py-1 rounded border border-neutral-600 hover:border-neutral-400"
|
||||||
|
>
|
||||||
|
{showCoreFeatures && showAdvancedFeatures ? 'Collapse All' : 'Expand All'}
|
||||||
|
</button>
|
||||||
|
<Badge className="bg-emerald-500/20 text-emerald-400 border-emerald-500/30">
|
||||||
|
v0.2.0 Compatible
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<a
|
||||||
|
href="https://google.github.io/A2A/specification"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center bg-blue-500/10 hover:bg-blue-500/20 px-4 py-2 rounded-lg border border-blue-500/20 transition-colors"
|
||||||
|
>
|
||||||
|
<FileText className="h-4 w-4 mr-2 text-blue-400" />
|
||||||
|
<span className="text-blue-400">View Official Specification</span>
|
||||||
|
<ExternalLink className="h-3 w-3 ml-2 text-blue-400" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-between cursor-pointer hover:bg-[#222]/30 p-2 rounded-lg transition-colors mb-4 border border-transparent hover:border-[#333]"
|
||||||
|
onClick={() => setShowCoreFeatures(!showCoreFeatures)}
|
||||||
|
>
|
||||||
|
<h3 className="text-white font-semibold flex items-center">
|
||||||
|
<CheckCircle2 className="h-4 w-4 mr-2 text-green-400" />
|
||||||
|
Core Features
|
||||||
|
<span className="ml-2 text-green-400 text-sm">({implementedCount}/{implementedFeatures.length} implemented)</span>
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-xs text-neutral-500">
|
||||||
|
{showCoreFeatures ? 'Hide details' : 'Show details'}
|
||||||
|
</span>
|
||||||
|
{showCoreFeatures ? (
|
||||||
|
<ChevronUp className="h-4 w-4 text-neutral-400 hover:text-white transition-colors" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="h-4 w-4 text-neutral-400 hover:text-white transition-colors" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showCoreFeatures && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
{implementedFeatures.map((feature, index) => (
|
||||||
|
<div key={index} className="flex items-start space-x-3 bg-[#222]/50 p-3 rounded-lg border border-[#333]">
|
||||||
|
{getStatusIcon(feature.status, feature.icon)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium text-white truncate">{feature.name}</p>
|
||||||
|
<p className="text-xs text-neutral-400">{feature.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-between cursor-pointer hover:bg-[#222]/30 p-2 rounded-lg transition-colors mb-4 border border-transparent hover:border-[#333]"
|
||||||
|
onClick={() => setShowAdvancedFeatures(!showAdvancedFeatures)}
|
||||||
|
>
|
||||||
|
<h3 className="text-white font-semibold flex items-center">
|
||||||
|
<Settings className="h-4 w-4 mr-2 text-blue-400" />
|
||||||
|
Advanced Features
|
||||||
|
<span className="ml-2 text-blue-400 text-sm">({advancedImplementedCount}/{advancedFeatures.length} implemented)</span>
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-xs text-neutral-500">
|
||||||
|
{showAdvancedFeatures ? 'Hide details' : 'Show details'}
|
||||||
|
</span>
|
||||||
|
{showAdvancedFeatures ? (
|
||||||
|
<ChevronUp className="h-4 w-4 text-neutral-400 hover:text-white transition-colors" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="h-4 w-4 text-neutral-400 hover:text-white transition-colors" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showAdvancedFeatures && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{advancedFeatures.map((feature, index) => (
|
||||||
|
<div key={index} className="flex items-start space-x-3 bg-[#222]/50 p-3 rounded-lg border border-[#333]">
|
||||||
|
{getStatusIcon(feature.status, feature.icon)}
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<p className="text-sm font-medium text-white">{feature.name}</p>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={`text-xs ${
|
||||||
|
feature.status === 'implemented' ? 'border-green-500 text-green-400' :
|
||||||
|
feature.status === 'partial' ? 'border-yellow-500 text-yellow-400' :
|
||||||
|
'border-blue-500 text-blue-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{feature.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-neutral-400 mt-1">{feature.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-emerald-500/10 border border-emerald-500/20 rounded-lg p-4">
|
||||||
|
<div className="flex items-start space-x-2">
|
||||||
|
<Shield className="h-4 w-4 text-emerald-400 mt-0.5" />
|
||||||
|
<div className="text-sm">
|
||||||
|
<p className="text-emerald-400 font-medium">✓ 100% A2A v0.2.0 Compliance Achieved</p>
|
||||||
|
<p className="text-emerald-300 mt-1">
|
||||||
|
All 8 official RPC methods implemented • Complete protocol data objects • Full workflow support • Enterprise security ready
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
78
frontend/app/documentation/components/CodeBlock.tsx
Normal file
78
frontend/app/documentation/components/CodeBlock.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/documentation/components/CodeBlock.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
import React from "react";
|
||||||
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
|
import { dracula } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ClipboardCopy } from "lucide-react";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
interface CodeBlockProps {
|
||||||
|
text: string;
|
||||||
|
language: string;
|
||||||
|
showLineNumbers?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CodeBlock({ text, language, showLineNumbers = true }: CodeBlockProps) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const copyToClipboard = () => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
toast({
|
||||||
|
title: "Copied!",
|
||||||
|
description: "Code copied to clipboard",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative rounded-md overflow-hidden">
|
||||||
|
<SyntaxHighlighter
|
||||||
|
language={language}
|
||||||
|
style={dracula}
|
||||||
|
showLineNumbers={showLineNumbers}
|
||||||
|
wrapLines={true}
|
||||||
|
customStyle={{
|
||||||
|
margin: 0,
|
||||||
|
padding: "1rem",
|
||||||
|
borderRadius: "0.375rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2 text-white hover:bg-[#333] opacity-80 hover:opacity-100"
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
>
|
||||||
|
<ClipboardCopy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
317
frontend/app/documentation/components/CodeExamplesSection.tsx
Normal file
317
frontend/app/documentation/components/CodeExamplesSection.tsx
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/documentation/components/CodeExamplesSection.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { CodeBlock } from "@/app/documentation/components/CodeBlock";
|
||||||
|
|
||||||
|
interface CodeExamplesSectionProps {
|
||||||
|
agentUrl: string;
|
||||||
|
apiKey: string;
|
||||||
|
jsonRpcRequest: any;
|
||||||
|
curlExample: string;
|
||||||
|
fetchExample: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CodeExamplesSection({
|
||||||
|
agentUrl,
|
||||||
|
apiKey,
|
||||||
|
jsonRpcRequest,
|
||||||
|
curlExample,
|
||||||
|
fetchExample
|
||||||
|
}: CodeExamplesSectionProps) {
|
||||||
|
const pythonExample = `import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
def test_a2a_agent():
|
||||||
|
url = "${agentUrl || "http://localhost:8000/api/v1/a2a/your-agent-id"}"
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-api-key": "${apiKey || "your-api-key"}"
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = ${JSON.stringify(jsonRpcRequest, null, 2)}
|
||||||
|
|
||||||
|
response = requests.post(url, headers=headers, json=payload)
|
||||||
|
data = response.json()
|
||||||
|
print("Agent response:", data)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_a2a_agent()`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-[#1a1a1a] border-[#333] text-white">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-emerald-400">Code Examples</CardTitle>
|
||||||
|
<CardDescription className="text-neutral-400">
|
||||||
|
Code snippets ready to use with A2A agents
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Tabs defaultValue="curl">
|
||||||
|
<TabsList className="bg-[#222] border-[#333] mb-4">
|
||||||
|
<TabsTrigger value="curl" className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400">
|
||||||
|
cURL
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="javascript" className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400">
|
||||||
|
JavaScript
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="python" className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400">
|
||||||
|
Python
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="curl" className="relative">
|
||||||
|
<CodeBlock
|
||||||
|
text={curlExample}
|
||||||
|
language="bash"
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="javascript" className="relative">
|
||||||
|
<CodeBlock
|
||||||
|
text={fetchExample}
|
||||||
|
language="javascript"
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="python" className="relative">
|
||||||
|
<CodeBlock
|
||||||
|
text={pythonExample}
|
||||||
|
language="python"
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<h3 className="text-xl font-semibold text-white mb-3">Sending files to the agent</h3>
|
||||||
|
<p className="text-neutral-400 mb-4">
|
||||||
|
You can attach files to messages sent to the agent using the A2A protocol.
|
||||||
|
The files are encoded in base64 and incorporated into the message as parts of type "file".
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-lg font-medium text-emerald-400 mb-2">Python</h4>
|
||||||
|
<CodeBlock
|
||||||
|
text={`import asyncio
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from common.client import A2ACardResolver, A2AClient
|
||||||
|
|
||||||
|
async def send_message_with_files():
|
||||||
|
# Instantiate client
|
||||||
|
card_resolver = A2ACardResolver("http://localhost:8000/api/v1/a2a/your-agent-id")
|
||||||
|
card = card_resolver.get_agent_card()
|
||||||
|
client = A2AClient(agent_card=card)
|
||||||
|
|
||||||
|
# Create session and task IDs
|
||||||
|
session_id = uuid4().hex
|
||||||
|
task_id = uuid4().hex
|
||||||
|
|
||||||
|
# Read file and encode in base64
|
||||||
|
file_path = "example.jpg"
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
file_content = base64.b64encode(f.read()).decode('utf-8')
|
||||||
|
file_name = os.path.basename(file_path)
|
||||||
|
|
||||||
|
# Create message with text and file
|
||||||
|
message = {
|
||||||
|
'role': 'user',
|
||||||
|
'parts': [
|
||||||
|
{
|
||||||
|
'type': 'text',
|
||||||
|
'text': 'Analyze this image for me',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'file',
|
||||||
|
'file': {
|
||||||
|
'name': file_name,
|
||||||
|
'bytes': file_content,
|
||||||
|
'mime_type': 'application/octet-stream' # Important: include the mime_type for correct file processing
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create request payload
|
||||||
|
payload = {
|
||||||
|
'id': task_id,
|
||||||
|
'sessionId': session_id,
|
||||||
|
'acceptedOutputModes': ['text'],
|
||||||
|
'message': message,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send request
|
||||||
|
task_result = await client.send_task(payload)
|
||||||
|
print(f'\\nResponse: {task_result.model_dump_json(exclude_none=True)}')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(send_message_with_files())`}
|
||||||
|
language="python"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-lg font-medium text-emerald-400 mb-2">JavaScript/TypeScript</h4>
|
||||||
|
<CodeBlock
|
||||||
|
text={`// Function to convert file to base64
|
||||||
|
async function fileToBase64(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const result = reader.result;
|
||||||
|
const base64 = result.split(',')[1];
|
||||||
|
resolve(base64);
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendMessageWithFiles() {
|
||||||
|
// Select file (in a web application)
|
||||||
|
const fileInput = document.getElementById('fileInput');
|
||||||
|
const files = fileInput.files;
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
console.error('No file selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert file to base64
|
||||||
|
const file = files[0];
|
||||||
|
const base64Data = await fileToBase64(file);
|
||||||
|
|
||||||
|
// Create session and task IDs
|
||||||
|
const sessionId = crypto.randomUUID();
|
||||||
|
const taskId = crypto.randomUUID();
|
||||||
|
const callId = crypto.randomUUID();
|
||||||
|
|
||||||
|
// Create message with text and file
|
||||||
|
const payload = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "message/send",
|
||||||
|
params: {
|
||||||
|
message: {
|
||||||
|
role: "user",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Analyze this document for me",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "file",
|
||||||
|
file: {
|
||||||
|
name: file.name,
|
||||||
|
bytes: base64Data,
|
||||||
|
mime_type: file.type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
sessionId: sessionId,
|
||||||
|
id: taskId,
|
||||||
|
},
|
||||||
|
id: callId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send request
|
||||||
|
const response = await fetch('http://localhost:8000/api/v1/a2a/your-agent-id', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-api-key': 'your-api-key',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('Agent response:', data);
|
||||||
|
}`}
|
||||||
|
language="javascript"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-lg font-medium text-emerald-400 mb-2">Curl</h4>
|
||||||
|
<CodeBlock
|
||||||
|
text={`# Convert file to base64
|
||||||
|
FILE_PATH="example.jpg"
|
||||||
|
FILE_NAME=$(basename $FILE_PATH)
|
||||||
|
BASE64_CONTENT=$(base64 -w 0 $FILE_PATH)
|
||||||
|
|
||||||
|
# Create request payload
|
||||||
|
read -r -d '' PAYLOAD << EOM
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "message/send",
|
||||||
|
"params": {
|
||||||
|
"message": {
|
||||||
|
"role": "user",
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "Analyze this image for me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"file": {
|
||||||
|
"name": "$FILE_NAME",
|
||||||
|
"bytes": "$BASE64_CONTENT",
|
||||||
|
"mime_type": "$(file --mime-type -b $FILE_PATH)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sessionId": "session-123",
|
||||||
|
"id": "task-456"
|
||||||
|
},
|
||||||
|
"id": "call-789"
|
||||||
|
}
|
||||||
|
EOM
|
||||||
|
|
||||||
|
# Send request
|
||||||
|
curl -X POST \\
|
||||||
|
http://localhost:8000/api/v1/a2a/your-agent-id \\
|
||||||
|
-H 'Content-Type: application/json' \\
|
||||||
|
-H 'x-api-key: your-api-key' \\
|
||||||
|
-d "$PAYLOAD"`}
|
||||||
|
language="bash"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
588
frontend/app/documentation/components/DocumentationSection.tsx
Normal file
588
frontend/app/documentation/components/DocumentationSection.tsx
Normal file
@@ -0,0 +1,588 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/documentation/components/DocumentationSection.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import {
|
||||||
|
ClipboardCopy,
|
||||||
|
Info,
|
||||||
|
ExternalLink,
|
||||||
|
Users,
|
||||||
|
Shield,
|
||||||
|
Zap,
|
||||||
|
Network,
|
||||||
|
FileText,
|
||||||
|
MessageSquare,
|
||||||
|
Settings,
|
||||||
|
AlertCircle,
|
||||||
|
CheckCircle2,
|
||||||
|
Globe
|
||||||
|
} from "lucide-react";
|
||||||
|
import { CodeBlock } from "@/app/documentation/components/CodeBlock";
|
||||||
|
|
||||||
|
interface DocumentationSectionProps {
|
||||||
|
copyToClipboard: (text: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DocumentationSection({ copyToClipboard }: DocumentationSectionProps) {
|
||||||
|
const quickStartExample = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: "req-001",
|
||||||
|
method: "message/send",
|
||||||
|
params: {
|
||||||
|
message: {
|
||||||
|
role: "user",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Hello! Can you help me analyze this data?"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
messageId: "6dbc13b5-bd57-4c2b-b503-24e381b6c8d6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const streamingExample = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: "req-002",
|
||||||
|
method: "message/stream",
|
||||||
|
params: {
|
||||||
|
message: {
|
||||||
|
role: "user",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Generate a detailed report on market trends"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
messageId: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileUploadExample = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: "req-003",
|
||||||
|
method: "message/send",
|
||||||
|
params: {
|
||||||
|
message: {
|
||||||
|
role: "user",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Analyze this image and highlight any faces."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "file",
|
||||||
|
file: {
|
||||||
|
name: "input_image.png",
|
||||||
|
mimeType: "image/png",
|
||||||
|
bytes: "iVBORw0KGgoAAAANSUhEUgAAAAUA..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
messageId: "8f0dc03c-4c65-4a14-9b56-7e8b9f2d1a3c"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<Card className="bg-gradient-to-br from-emerald-500/10 to-blue-500/10 border-emerald-500/20 text-white">
|
||||||
|
<CardHeader className="text-center">
|
||||||
|
<div className="flex justify-center mb-4">
|
||||||
|
<div className="flex items-center space-x-2 bg-emerald-500/20 px-4 py-2 rounded-full">
|
||||||
|
<Network className="h-6 w-6 text-emerald-400" />
|
||||||
|
<span className="font-bold text-emerald-400">Agent2Agent Protocol</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CardTitle className="text-3xl font-bold bg-gradient-to-r from-emerald-400 to-blue-400 bg-clip-text text-transparent">
|
||||||
|
The Standard for AI Agent Communication
|
||||||
|
</CardTitle>
|
||||||
|
<p className="text-lg text-neutral-300 mt-4 max-w-3xl mx-auto">
|
||||||
|
A2A is Google's open protocol enabling seamless communication and interoperability
|
||||||
|
between AI agents across different platforms, providers, and architectures.
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-wrap justify-center gap-4 mt-6">
|
||||||
|
<a
|
||||||
|
href="https://google.github.io/A2A/specification"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center bg-emerald-500/20 hover:bg-emerald-500/30 px-4 py-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<FileText className="h-4 w-4 mr-2 text-emerald-400" />
|
||||||
|
<span className="text-emerald-400">Official Specification</span>
|
||||||
|
<ExternalLink className="h-3 w-3 ml-2 text-emerald-400" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://github.com/google/A2A"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center bg-blue-500/20 hover:bg-blue-500/30 px-4 py-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Globe className="h-4 w-4 mr-2 text-blue-400" />
|
||||||
|
<span className="text-blue-400">GitHub Repository</span>
|
||||||
|
<ExternalLink className="h-3 w-3 ml-2 text-blue-400" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Key Features */}
|
||||||
|
<Card className="bg-[#1a1a1a] border-[#333] text-white">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-emerald-400 flex items-center">
|
||||||
|
<Zap className="h-5 w-5 mr-2" />
|
||||||
|
Key Features & Capabilities
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="bg-emerald-500/20 p-2 rounded-lg">
|
||||||
|
<MessageSquare className="h-5 w-5 text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-white">Multi-turn Conversations</h3>
|
||||||
|
<p className="text-sm text-neutral-400">Support for complex, contextual dialogues between agents</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="bg-blue-500/20 p-2 rounded-lg">
|
||||||
|
<FileText className="h-5 w-5 text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-white">File Exchange</h3>
|
||||||
|
<p className="text-sm text-neutral-400">Upload and download files with proper MIME type handling</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="bg-purple-500/20 p-2 rounded-lg">
|
||||||
|
<Zap className="h-5 w-5 text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-white">Real-time Streaming</h3>
|
||||||
|
<p className="text-sm text-neutral-400">Server-Sent Events for live response streaming</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="bg-orange-500/20 p-2 rounded-lg">
|
||||||
|
<Settings className="h-5 w-5 text-orange-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-white">Task Management</h3>
|
||||||
|
<p className="text-sm text-neutral-400">Track, query, and cancel long-running tasks</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="bg-red-500/20 p-2 rounded-lg">
|
||||||
|
<Shield className="h-5 w-5 text-red-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-white">Enterprise Security</h3>
|
||||||
|
<p className="text-sm text-neutral-400">Bearer tokens, API keys, and HTTPS enforcement</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="bg-green-500/20 p-2 rounded-lg">
|
||||||
|
<Users className="h-5 w-5 text-green-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-white">Agent Discovery</h3>
|
||||||
|
<p className="text-sm text-neutral-400">Standardized agent cards for capability discovery</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Protocol Methods */}
|
||||||
|
<Card className="bg-[#1a1a1a] border-[#333] text-white">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-emerald-400">Protocol Methods</CardTitle>
|
||||||
|
<p className="text-neutral-400">A2A supports multiple RPC methods for different interaction patterns</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Tabs defaultValue="messaging" className="w-full">
|
||||||
|
<TabsList className="grid w-full grid-cols-3 bg-[#222] border-[#444]">
|
||||||
|
<TabsTrigger value="messaging" className="data-[state=active]:bg-emerald-500/20 data-[state=active]:text-emerald-400">
|
||||||
|
Messaging
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="tasks" className="data-[state=active]:bg-emerald-500/20 data-[state=active]:text-emerald-400">
|
||||||
|
Task Management
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="discovery" className="data-[state=active]:bg-emerald-500/20 data-[state=active]:text-emerald-400">
|
||||||
|
Discovery
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="messaging" className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="bg-[#222] p-4 rounded-lg border border-[#444]">
|
||||||
|
<div className="flex items-center space-x-2 mb-3">
|
||||||
|
<Badge variant="outline" className="border-emerald-500 text-emerald-400">message/send</Badge>
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
<h4 className="font-semibold text-white mb-2">Standard HTTP Request</h4>
|
||||||
|
<p className="text-sm text-neutral-400 mb-3">
|
||||||
|
Send a message and receive a complete response after processing is finished.
|
||||||
|
</p>
|
||||||
|
<ul className="text-xs text-neutral-400 space-y-1">
|
||||||
|
<li>• Single request/response cycle</li>
|
||||||
|
<li>• Best for simple queries</li>
|
||||||
|
<li>• Lower complexity implementation</li>
|
||||||
|
<li>• Synchronous operation</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-[#222] p-4 rounded-lg border border-[#444]">
|
||||||
|
<div className="flex items-center space-x-2 mb-3">
|
||||||
|
<Badge variant="outline" className="border-blue-500 text-blue-400">message/stream</Badge>
|
||||||
|
<Zap className="h-4 w-4 text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<h4 className="font-semibold text-white mb-2">Real-time Streaming</h4>
|
||||||
|
<p className="text-sm text-neutral-400 mb-3">
|
||||||
|
Receive partial responses in real-time via Server-Sent Events.
|
||||||
|
</p>
|
||||||
|
<ul className="text-xs text-neutral-400 space-y-1">
|
||||||
|
<li>• Progressive response delivery</li>
|
||||||
|
<li>• Better UX for long tasks</li>
|
||||||
|
<li>• Live status updates</li>
|
||||||
|
<li>• Asynchronous operation</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="tasks" className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="bg-[#222] p-4 rounded-lg border border-[#444]">
|
||||||
|
<div className="flex items-center space-x-2 mb-3">
|
||||||
|
<Badge variant="outline" className="border-purple-500 text-purple-400">tasks/get</Badge>
|
||||||
|
<Settings className="h-4 w-4 text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<h4 className="font-semibold text-white mb-2">Query Task Status</h4>
|
||||||
|
<p className="text-sm text-neutral-400 mb-3">
|
||||||
|
Check the status, progress, and results of a specific task.
|
||||||
|
</p>
|
||||||
|
<ul className="text-xs text-neutral-400 space-y-1">
|
||||||
|
<li>• Real-time status checking</li>
|
||||||
|
<li>• Progress monitoring</li>
|
||||||
|
<li>• Result retrieval</li>
|
||||||
|
<li>• Error diagnosis</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-[#222] p-4 rounded-lg border border-[#444]">
|
||||||
|
<div className="flex items-center space-x-2 mb-3">
|
||||||
|
<Badge variant="outline" className="border-red-500 text-red-400">tasks/cancel</Badge>
|
||||||
|
<AlertCircle className="h-4 w-4 text-red-400" />
|
||||||
|
</div>
|
||||||
|
<h4 className="font-semibold text-white mb-2">Cancel Task</h4>
|
||||||
|
<p className="text-sm text-neutral-400 mb-3">
|
||||||
|
Terminate a running task before completion.
|
||||||
|
</p>
|
||||||
|
<ul className="text-xs text-neutral-400 space-y-1">
|
||||||
|
<li>• Graceful task termination</li>
|
||||||
|
<li>• Resource cleanup</li>
|
||||||
|
<li>• Cost optimization</li>
|
||||||
|
<li>• User control</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="discovery" className="space-y-4">
|
||||||
|
<div className="bg-[#222] p-4 rounded-lg border border-[#444]">
|
||||||
|
<div className="flex items-center space-x-2 mb-3">
|
||||||
|
<Badge variant="outline" className="border-green-500 text-green-400">agent/authenticatedExtendedCard</Badge>
|
||||||
|
<Users className="h-4 w-4 text-green-400" />
|
||||||
|
</div>
|
||||||
|
<h4 className="font-semibold text-white mb-2">Agent Discovery</h4>
|
||||||
|
<p className="text-sm text-neutral-400 mb-3">
|
||||||
|
Retrieve detailed information about agent capabilities, skills, and requirements.
|
||||||
|
</p>
|
||||||
|
<ul className="text-xs text-neutral-400 space-y-1">
|
||||||
|
<li>• Agent capability discovery</li>
|
||||||
|
<li>• Skill and tool enumeration</li>
|
||||||
|
<li>• Authentication requirements</li>
|
||||||
|
<li>• API version compatibility</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Code Examples */}
|
||||||
|
<Card className="bg-[#1a1a1a] border-[#333] text-white">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-emerald-400">Quick Start Examples</CardTitle>
|
||||||
|
<p className="text-neutral-400">Ready-to-use JSON-RPC examples based on the official A2A specification</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Tabs defaultValue="basic" className="w-full">
|
||||||
|
<TabsList className="grid w-full grid-cols-3 bg-[#222] border-[#444]">
|
||||||
|
<TabsTrigger value="basic" className="data-[state=active]:bg-emerald-500/20 data-[state=active]:text-emerald-400">
|
||||||
|
Basic Message
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="streaming" className="data-[state=active]:bg-emerald-500/20 data-[state=active]:text-emerald-400">
|
||||||
|
Streaming
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="files" className="data-[state=active]:bg-emerald-500/20 data-[state=active]:text-emerald-400">
|
||||||
|
File Upload
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="basic" className="space-y-4">
|
||||||
|
<div className="relative">
|
||||||
|
<CodeBlock
|
||||||
|
text={JSON.stringify(quickStartExample, null, 2)}
|
||||||
|
language="json"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||||
|
onClick={() => copyToClipboard(JSON.stringify(quickStartExample, null, 2))}
|
||||||
|
>
|
||||||
|
<ClipboardCopy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4">
|
||||||
|
<div className="flex items-start space-x-2">
|
||||||
|
<Info className="h-4 w-4 text-blue-400 mt-0.5" />
|
||||||
|
<div className="text-sm">
|
||||||
|
<p className="text-blue-400 font-medium">Key Points:</p>
|
||||||
|
<ul className="text-blue-300 mt-1 space-y-1">
|
||||||
|
<li>• Uses <code className="bg-blue-500/20 px-1 rounded">message/send</code> for standard HTTP requests</li>
|
||||||
|
<li>• <code className="bg-blue-500/20 px-1 rounded">messageId</code> must be a valid UUID v4</li>
|
||||||
|
<li>• Response contains task ID, status, and artifacts</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="streaming" className="space-y-4">
|
||||||
|
<div className="relative">
|
||||||
|
<CodeBlock
|
||||||
|
text={JSON.stringify(streamingExample, null, 2)}
|
||||||
|
language="json"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||||
|
onClick={() => copyToClipboard(JSON.stringify(streamingExample, null, 2))}
|
||||||
|
>
|
||||||
|
<ClipboardCopy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="bg-purple-500/10 border border-purple-500/20 rounded-lg p-4">
|
||||||
|
<div className="flex items-start space-x-2">
|
||||||
|
<Zap className="h-4 w-4 text-purple-400 mt-0.5" />
|
||||||
|
<div className="text-sm">
|
||||||
|
<p className="text-purple-400 font-medium">Streaming Features:</p>
|
||||||
|
<ul className="text-purple-300 mt-1 space-y-1">
|
||||||
|
<li>• Real-time Server-Sent Events (SSE)</li>
|
||||||
|
<li>• Progressive content delivery</li>
|
||||||
|
<li>• Status updates: submitted → working → completed</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="files" className="space-y-4">
|
||||||
|
<div className="relative">
|
||||||
|
<CodeBlock
|
||||||
|
text={JSON.stringify(fileUploadExample, null, 2)}
|
||||||
|
language="json"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||||
|
onClick={() => copyToClipboard(JSON.stringify(fileUploadExample, null, 2))}
|
||||||
|
>
|
||||||
|
<ClipboardCopy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-4">
|
||||||
|
<div className="flex items-start space-x-2">
|
||||||
|
<FileText className="h-4 w-4 text-green-400 mt-0.5" />
|
||||||
|
<div className="text-sm">
|
||||||
|
<p className="text-green-400 font-medium">File Handling:</p>
|
||||||
|
<ul className="text-green-300 mt-1 space-y-1">
|
||||||
|
<li>• Support for multiple file types (images, documents, etc.)</li>
|
||||||
|
<li>• Base64 encoding for binary data</li>
|
||||||
|
<li>• Proper MIME type specification</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Security & Best Practices */}
|
||||||
|
<Card className="bg-[#1a1a1a] border-[#333] text-white">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-emerald-400 flex items-center">
|
||||||
|
<Shield className="h-5 w-5 mr-2" />
|
||||||
|
Security & Best Practices
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-white font-semibold mb-3 flex items-center">
|
||||||
|
<Shield className="h-4 w-4 mr-2 text-emerald-400" />
|
||||||
|
Authentication Methods
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="bg-[#222] p-3 rounded-lg border border-[#444]">
|
||||||
|
<div className="flex items-center space-x-2 mb-2">
|
||||||
|
<code className="text-emerald-400 text-sm">x-api-key</code>
|
||||||
|
<Badge variant="outline" className="text-xs">Recommended</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-neutral-400">Custom header for API key authentication</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#222] p-3 rounded-lg border border-[#444]">
|
||||||
|
<div className="flex items-center space-x-2 mb-2">
|
||||||
|
<code className="text-blue-400 text-sm">Authorization: Bearer</code>
|
||||||
|
<Badge variant="outline" className="text-xs">Standard</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-neutral-400">OAuth 2.0 Bearer token authentication</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-white font-semibold mb-3 flex items-center">
|
||||||
|
<AlertCircle className="h-4 w-4 mr-2 text-orange-400" />
|
||||||
|
Security Requirements
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<CheckCircle2 className="h-3 w-3 text-green-400" />
|
||||||
|
<span className="text-neutral-300">HTTPS/TLS encryption required</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<CheckCircle2 className="h-3 w-3 text-green-400" />
|
||||||
|
<span className="text-neutral-300">Input validation on all parameters</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<CheckCircle2 className="h-3 w-3 text-green-400" />
|
||||||
|
<span className="text-neutral-300">Rate limiting and resource controls</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<CheckCircle2 className="h-3 w-3 text-green-400" />
|
||||||
|
<span className="text-neutral-300">Proper CORS configuration</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-amber-500/10 border border-amber-500/20 rounded-lg p-4">
|
||||||
|
<div className="flex items-start space-x-2">
|
||||||
|
<AlertCircle className="h-4 w-4 text-amber-400 mt-0.5" />
|
||||||
|
<div className="text-sm">
|
||||||
|
<p className="text-amber-400 font-medium">Important:</p>
|
||||||
|
<p className="text-amber-300 mt-1">
|
||||||
|
Always obtain API credentials out-of-band. Never include sensitive authentication
|
||||||
|
data in client-side code or version control systems.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* A2A vs MCP */}
|
||||||
|
<Card className="bg-[#1a1a1a] border-[#333] text-white">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-emerald-400 flex items-center">
|
||||||
|
<Network className="h-5 w-5 mr-2" />
|
||||||
|
A2A vs Model Context Protocol (MCP)
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-[#222] border-b border-[#444]">
|
||||||
|
<th className="p-4 text-left text-neutral-300">Aspect</th>
|
||||||
|
<th className="p-4 text-left text-emerald-400">Agent2Agent (A2A)</th>
|
||||||
|
<th className="p-4 text-left text-blue-400">Model Context Protocol (MCP)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr className="border-b border-[#333]">
|
||||||
|
<td className="p-4 text-neutral-300 font-medium">Purpose</td>
|
||||||
|
<td className="p-4 text-neutral-300">Agent-to-agent communication</td>
|
||||||
|
<td className="p-4 text-neutral-300">Model-to-tool/resource integration</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="border-b border-[#333]">
|
||||||
|
<td className="p-4 text-neutral-300 font-medium">Use Case</td>
|
||||||
|
<td className="p-4 text-neutral-300">AI agents collaborating as peers</td>
|
||||||
|
<td className="p-4 text-neutral-300">AI models accessing external capabilities</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="border-b border-[#333]">
|
||||||
|
<td className="p-4 text-neutral-300 font-medium">Relationship</td>
|
||||||
|
<td className="p-4 text-neutral-300">Partner/delegate work</td>
|
||||||
|
<td className="p-4 text-neutral-300">Use specific capabilities</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="border-b border-[#333]">
|
||||||
|
<td className="p-4 text-neutral-300 font-medium">Integration</td>
|
||||||
|
<td className="p-4 text-neutral-300 text-emerald-400">✓ Can use MCP internally</td>
|
||||||
|
<td className="p-4 text-neutral-300 text-blue-400">✓ Complements A2A</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 bg-blue-500/10 border border-blue-500/20 rounded-lg p-4">
|
||||||
|
<p className="text-blue-300 text-sm">
|
||||||
|
<strong>Working Together:</strong> An A2A client agent might request an A2A server agent to perform a complex task.
|
||||||
|
The server agent, in turn, might use MCP to interact with tools, APIs, or data sources necessary to fulfill the A2A task.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,796 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/documentation/components/FrontendImplementationSection.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ClipboardCopy } from "lucide-react";
|
||||||
|
import { CodeBlock } from "@/app/documentation/components/CodeBlock";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
|
||||||
|
interface FrontendImplementationSectionProps {
|
||||||
|
copyToClipboard: (text: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FrontendImplementationSection({ copyToClipboard }: FrontendImplementationSectionProps) {
|
||||||
|
return (
|
||||||
|
<Card className="bg-[#1a1a1a] border-[#333] text-white">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-emerald-400">Frontend implementation</CardTitle>
|
||||||
|
<CardDescription className="text-neutral-400">
|
||||||
|
Practical examples for implementation in React applications
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<Tabs defaultValue="standard">
|
||||||
|
<TabsList className="bg-[#222] border-[#333] mb-4">
|
||||||
|
<TabsTrigger value="standard" className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400">
|
||||||
|
Standard HTTP
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="streaming" className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400">
|
||||||
|
Streaming SSE
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="react-component" className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400">
|
||||||
|
React component
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="standard">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-emerald-400 text-lg font-medium mb-2">Implementation of message/send</h3>
|
||||||
|
<p className="text-neutral-300 mb-4">
|
||||||
|
Example of standard implementation in JavaScript/React:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<CodeBlock
|
||||||
|
text={`async function sendTask(agentId, message) {
|
||||||
|
// Generate unique IDs
|
||||||
|
const taskId = crypto.randomUUID();
|
||||||
|
const callId = crypto.randomUUID();
|
||||||
|
|
||||||
|
// Prepare request data
|
||||||
|
const requestData = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: callId,
|
||||||
|
method: "message/send",
|
||||||
|
params: {
|
||||||
|
id: taskId,
|
||||||
|
sessionId: "session-" + Math.random().toString(36).substring(2, 9),
|
||||||
|
message: {
|
||||||
|
role: "user",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: message
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Indicate loading state
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
// Send the request
|
||||||
|
const response = await fetch(\`/api/v1/a2a/\${agentId}\`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-api-key': 'YOUR_API_KEY_HERE'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(\`HTTP error: \${response.status}\`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the response
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Check for errors
|
||||||
|
if (data.error) {
|
||||||
|
console.error('Error in response:', data.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the agent response
|
||||||
|
const task = data.result;
|
||||||
|
|
||||||
|
// Show response in UI
|
||||||
|
if (task.status.message && task.status.message.parts) {
|
||||||
|
const responseText = task.status.message.parts
|
||||||
|
.filter(part => part.type === 'text')
|
||||||
|
.map(part => part.text)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
// Here you would update your React state
|
||||||
|
// setResponse(responseText);
|
||||||
|
|
||||||
|
return responseText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return task;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending task:', error);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
// Finalize loading state
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}`}
|
||||||
|
language="javascript"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||||
|
onClick={() => copyToClipboard(`async function sendTask(agentId, message) {
|
||||||
|
// Generate unique IDs
|
||||||
|
const taskId = crypto.randomUUID();
|
||||||
|
const callId = crypto.randomUUID();
|
||||||
|
|
||||||
|
// Prepare request data
|
||||||
|
const requestData = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: callId,
|
||||||
|
method: "message/send",
|
||||||
|
params: {
|
||||||
|
id: taskId,
|
||||||
|
sessionId: "session-" + Math.random().toString(36).substring(2, 9),
|
||||||
|
message: {
|
||||||
|
role: "user",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: message
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Indicate loading state
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
// Send the request
|
||||||
|
const response = await fetch(\`/api/v1/a2a/\${agentId}\`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-api-key': 'YOUR_API_KEY_HERE'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(\`HTTP error: \${response.status}\`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the response
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Check for errors
|
||||||
|
if (data.error) {
|
||||||
|
console.error('Error in response:', data.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the agent response
|
||||||
|
const task = data.result;
|
||||||
|
|
||||||
|
// Show response in UI
|
||||||
|
if (task.status.message && task.status.message.parts) {
|
||||||
|
const responseText = task.status.message.parts
|
||||||
|
.filter(part => part.type === 'text')
|
||||||
|
.map(part => part.text)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
// Here you would update your React state
|
||||||
|
// setResponse(responseText);
|
||||||
|
|
||||||
|
return responseText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return task;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending task:', error);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
// Finalize loading state
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}`)}
|
||||||
|
>
|
||||||
|
<ClipboardCopy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="streaming">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-emerald-400 text-lg font-medium mb-2">Implementation of message/stream (Streaming)</h3>
|
||||||
|
<p className="text-neutral-300 mb-4">
|
||||||
|
Example of implementation of streaming with Server-Sent Events (SSE):
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<CodeBlock
|
||||||
|
text={`async function initAgentStream(agentId, message, onUpdateCallback) {
|
||||||
|
// Generate unique IDs
|
||||||
|
const taskId = crypto.randomUUID();
|
||||||
|
const callId = crypto.randomUUID();
|
||||||
|
const sessionId = "session-" + Math.random().toString(36).substring(2, 9);
|
||||||
|
|
||||||
|
// Prepare request data for streaming
|
||||||
|
const requestData = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: callId,
|
||||||
|
method: "message/stream",
|
||||||
|
params: {
|
||||||
|
id: taskId,
|
||||||
|
sessionId: sessionId,
|
||||||
|
message: {
|
||||||
|
role: "user",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: message
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Start initial POST request
|
||||||
|
const response = await fetch(\`/api/v1/a2a/\${agentId}\`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-api-key': 'YOUR_API_KEY_HERE',
|
||||||
|
'Accept': 'text/event-stream' // Important for SSE
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(\`HTTP error: \${response.status}\`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check content type of the response
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
|
||||||
|
// If the response is already SSE, use EventSource
|
||||||
|
if (contentType?.includes('text/event-stream')) {
|
||||||
|
// Use EventSource to process the stream
|
||||||
|
setupEventSource(\`/api/v1/a2a/\${agentId}/stream?taskId=\${taskId}&key=YOUR_API_KEY_HERE\`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to configure EventSource
|
||||||
|
function setupEventSource(url) {
|
||||||
|
const eventSource = new EventSource(url);
|
||||||
|
|
||||||
|
// Handler for received messages
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
// Process data from the event
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
// Process the event
|
||||||
|
if (data.result) {
|
||||||
|
// Process status if available
|
||||||
|
if (data.result.status) {
|
||||||
|
const status = data.result.status;
|
||||||
|
|
||||||
|
// Extract text if available
|
||||||
|
let currentText = '';
|
||||||
|
if (status.message && status.message.parts) {
|
||||||
|
const parts = status.message.parts.filter(part => part.type === 'text');
|
||||||
|
if (parts.length > 0) {
|
||||||
|
currentText = parts.map(part => part.text).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call callback with updates
|
||||||
|
onUpdateCallback({
|
||||||
|
text: currentText,
|
||||||
|
state: status.state,
|
||||||
|
complete: data.result.final === true
|
||||||
|
});
|
||||||
|
|
||||||
|
// If it's the final event, close the connection
|
||||||
|
if (data.result.final === true) {
|
||||||
|
eventSource.close();
|
||||||
|
onUpdateCallback({
|
||||||
|
complete: true,
|
||||||
|
state: status.state
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process artifact if available
|
||||||
|
if (data.result.artifact) {
|
||||||
|
const artifact = data.result.artifact;
|
||||||
|
if (artifact.parts && artifact.parts.length > 0) {
|
||||||
|
const parts = artifact.parts.filter(part => part.type === 'text');
|
||||||
|
if (parts.length > 0) {
|
||||||
|
const artifactText = parts.map(part => part.text).join('');
|
||||||
|
|
||||||
|
// Call callback with artifact
|
||||||
|
onUpdateCallback({
|
||||||
|
text: artifactText,
|
||||||
|
state: 'artifact',
|
||||||
|
complete: artifact.lastChunk === true
|
||||||
|
});
|
||||||
|
|
||||||
|
// If it's the last chunk, close the connection
|
||||||
|
if (artifact.lastChunk === true) {
|
||||||
|
eventSource.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing event:', error);
|
||||||
|
onUpdateCallback({ error: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handler for errors
|
||||||
|
eventSource.onerror = (error) => {
|
||||||
|
console.error('Error in EventSource:', error);
|
||||||
|
eventSource.close();
|
||||||
|
onUpdateCallback({
|
||||||
|
error: 'Connection with server interrupted',
|
||||||
|
complete: true,
|
||||||
|
state: 'error'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in streaming:', error);
|
||||||
|
// Notify error through callback
|
||||||
|
onUpdateCallback({
|
||||||
|
error: error.message,
|
||||||
|
complete: true,
|
||||||
|
state: 'error'
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}`}
|
||||||
|
language="javascript"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||||
|
onClick={() => copyToClipboard(`async function initAgentStream(agentId, message, onUpdateCallback) {
|
||||||
|
// Generate unique IDs
|
||||||
|
const taskId = crypto.randomUUID();
|
||||||
|
const callId = crypto.randomUUID();
|
||||||
|
const sessionId = "session-" + Math.random().toString(36).substring(2, 9);
|
||||||
|
|
||||||
|
// Prepare request data for streaming
|
||||||
|
const requestData = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: callId,
|
||||||
|
method: "message/stream",
|
||||||
|
params: {
|
||||||
|
id: taskId,
|
||||||
|
sessionId: sessionId,
|
||||||
|
message: {
|
||||||
|
role: "user",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: message
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Start initial POST request
|
||||||
|
const response = await fetch(\`/api/v1/a2a/\${agentId}\`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-api-key': 'YOUR_API_KEY_HERE',
|
||||||
|
'Accept': 'text/event-stream' // Important for SSE
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(\`HTTP error: \${response.status}\`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check content type of the response
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
|
||||||
|
// If the response is already SSE, use EventSource
|
||||||
|
if (contentType?.includes('text/event-stream')) {
|
||||||
|
// Use EventSource to process the stream
|
||||||
|
setupEventSource(\`/api/v1/a2a/\${agentId}/stream?taskId=\${taskId}&key=YOUR_API_KEY_HERE\`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to configure EventSource
|
||||||
|
function setupEventSource(url) {
|
||||||
|
const eventSource = new EventSource(url);
|
||||||
|
|
||||||
|
// Handler for received messages
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
// Process data from the event
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
// Process the event
|
||||||
|
if (data.result) {
|
||||||
|
// Process status if available
|
||||||
|
if (data.result.status) {
|
||||||
|
const status = data.result.status;
|
||||||
|
|
||||||
|
// Extract text if available
|
||||||
|
let currentText = '';
|
||||||
|
if (status.message && status.message.parts) {
|
||||||
|
const parts = status.message.parts.filter(part => part.type === 'text');
|
||||||
|
if (parts.length > 0) {
|
||||||
|
currentText = parts.map(part => part.text).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call callback with updates
|
||||||
|
onUpdateCallback({
|
||||||
|
text: currentText,
|
||||||
|
state: status.state,
|
||||||
|
complete: data.result.final === true
|
||||||
|
});
|
||||||
|
|
||||||
|
// If it's the final event, close the connection
|
||||||
|
if (data.result.final === true) {
|
||||||
|
eventSource.close();
|
||||||
|
onUpdateCallback({
|
||||||
|
complete: true,
|
||||||
|
state: status.state
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process artifact if available
|
||||||
|
if (data.result.artifact) {
|
||||||
|
const artifact = data.result.artifact;
|
||||||
|
if (artifact.parts && artifact.parts.length > 0) {
|
||||||
|
const parts = artifact.parts.filter(part => part.type === 'text');
|
||||||
|
if (parts.length > 0) {
|
||||||
|
const artifactText = parts.map(part => part.text).join('');
|
||||||
|
|
||||||
|
// Call callback with artifact
|
||||||
|
onUpdateCallback({
|
||||||
|
text: artifactText,
|
||||||
|
state: 'artifact',
|
||||||
|
complete: artifact.lastChunk === true
|
||||||
|
});
|
||||||
|
|
||||||
|
// If it's the last chunk, close the connection
|
||||||
|
if (artifact.lastChunk === true) {
|
||||||
|
eventSource.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing event:', error);
|
||||||
|
onUpdateCallback({ error: error.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handler for errors
|
||||||
|
eventSource.onerror = (error) => {
|
||||||
|
console.error('Error in EventSource:', error);
|
||||||
|
eventSource.close();
|
||||||
|
onUpdateCallback({
|
||||||
|
error: 'Connection with server interrupted',
|
||||||
|
complete: true,
|
||||||
|
state: 'error'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in streaming:', error);
|
||||||
|
// Notify error through callback
|
||||||
|
onUpdateCallback({
|
||||||
|
error: error.message,
|
||||||
|
complete: true,
|
||||||
|
state: 'error'
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}`)}
|
||||||
|
>
|
||||||
|
<ClipboardCopy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="react-component">
|
||||||
|
<div className="mt-4">
|
||||||
|
<h4 className="font-medium text-emerald-400 mb-2">React component with streaming support:</h4>
|
||||||
|
<div className="relative">
|
||||||
|
<CodeBlock
|
||||||
|
text={`import React, { useState, useRef } from 'react';
|
||||||
|
|
||||||
|
function ChatComponentA2A() {
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
const [response, setResponse] = useState('');
|
||||||
|
const [isStreaming, setIsStreaming] = useState(false);
|
||||||
|
const [status, setStatus] = useState('');
|
||||||
|
|
||||||
|
// Reference to the agentId
|
||||||
|
const agentId = 'your-agent-id';
|
||||||
|
|
||||||
|
// Callback for streaming updates
|
||||||
|
const handleStreamUpdate = (update) => {
|
||||||
|
if (update.error) {
|
||||||
|
// Handle error
|
||||||
|
setStatus('error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update text
|
||||||
|
setResponse(update.text);
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
setStatus(update.state);
|
||||||
|
|
||||||
|
// If it's complete, finish streaming
|
||||||
|
if (update.complete) {
|
||||||
|
setIsStreaming(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!message.trim()) return;
|
||||||
|
|
||||||
|
// Clear previous response
|
||||||
|
setResponse('');
|
||||||
|
setStatus('submitted');
|
||||||
|
|
||||||
|
// Start streaming
|
||||||
|
setIsStreaming(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Start stream with the agent
|
||||||
|
await initAgentStream(agentId, message, handleStreamUpdate);
|
||||||
|
|
||||||
|
// Clear message field after sending
|
||||||
|
setMessage('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
setStatus('error');
|
||||||
|
setIsStreaming(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render status indicator based on status
|
||||||
|
const renderStatusIndicator = () => {
|
||||||
|
switch (status) {
|
||||||
|
case 'submitted':
|
||||||
|
return <span className="badge badge-info">Sent</span>;
|
||||||
|
case 'working':
|
||||||
|
return <span className="badge badge-warning">Processing</span>;
|
||||||
|
case 'completed':
|
||||||
|
return <span className="badge badge-success">Completed</span>;
|
||||||
|
case 'error':
|
||||||
|
return <span className="badge badge-danger">Error</span>;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="chat-container">
|
||||||
|
<div className="chat-messages">
|
||||||
|
{response && (
|
||||||
|
<div className="message agent-message">
|
||||||
|
<div className="message-header">
|
||||||
|
<div className="agent-name">A2A Agent</div>
|
||||||
|
{renderStatusIndicator()}
|
||||||
|
</div>
|
||||||
|
<div className="message-content">
|
||||||
|
{response}
|
||||||
|
{status === 'working' && !response && (
|
||||||
|
<div className="typing-indicator">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="chat-input-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
placeholder="Type your message..."
|
||||||
|
disabled={isStreaming}
|
||||||
|
className="chat-input"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isStreaming || !message.trim()}
|
||||||
|
className="send-button"
|
||||||
|
>
|
||||||
|
{isStreaming ? 'Processing...' : 'Send'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}`}
|
||||||
|
language="javascript"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||||
|
onClick={() => copyToClipboard(`import React, { useState, useRef } from 'react';
|
||||||
|
|
||||||
|
function ChatComponentA2A() {
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
const [response, setResponse] = useState('');
|
||||||
|
const [isStreaming, setIsStreaming] = useState(false);
|
||||||
|
const [status, setStatus] = useState('');
|
||||||
|
|
||||||
|
// Reference to the agentId
|
||||||
|
const agentId = 'your-agent-id';
|
||||||
|
|
||||||
|
// Callback for streaming updates
|
||||||
|
const handleStreamUpdate = (update) => {
|
||||||
|
if (update.error) {
|
||||||
|
// Handle error
|
||||||
|
setStatus('error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update text
|
||||||
|
setResponse(update.text);
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
setStatus(update.state);
|
||||||
|
|
||||||
|
// If it's complete, finish streaming
|
||||||
|
if (update.complete) {
|
||||||
|
setIsStreaming(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!message.trim()) return;
|
||||||
|
|
||||||
|
// Clear previous response
|
||||||
|
setResponse('');
|
||||||
|
setStatus('submitted');
|
||||||
|
|
||||||
|
// Start streaming
|
||||||
|
setIsStreaming(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Start stream with the agent
|
||||||
|
await initAgentStream(agentId, message, handleStreamUpdate);
|
||||||
|
|
||||||
|
// Clear message field after sending
|
||||||
|
setMessage('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
setStatus('error');
|
||||||
|
setIsStreaming(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render status indicator based on status
|
||||||
|
const renderStatusIndicator = () => {
|
||||||
|
switch (status) {
|
||||||
|
case 'submitted':
|
||||||
|
return <span className="badge badge-info">Sent</span>;
|
||||||
|
case 'working':
|
||||||
|
return <span className="badge badge-warning">Processing</span>;
|
||||||
|
case 'completed':
|
||||||
|
return <span className="badge badge-success">Completed</span>;
|
||||||
|
case 'error':
|
||||||
|
return <span className="badge badge-danger">Error</span>;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="chat-container">
|
||||||
|
<div className="chat-messages">
|
||||||
|
{response && (
|
||||||
|
<div className="message agent-message">
|
||||||
|
<div className="message-header">
|
||||||
|
<div className="agent-name">A2A Agent</div>
|
||||||
|
{renderStatusIndicator()}
|
||||||
|
</div>
|
||||||
|
<div className="message-content">
|
||||||
|
{response}
|
||||||
|
{status === 'working' && !response && (
|
||||||
|
<div className="typing-indicator">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="chat-input-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
placeholder="Type your message..."
|
||||||
|
disabled={isStreaming}
|
||||||
|
className="chat-input"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isStreaming || !message.trim()}
|
||||||
|
className="send-button"
|
||||||
|
>
|
||||||
|
{isStreaming ? 'Processing...' : 'Send'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}`)}
|
||||||
|
>
|
||||||
|
<ClipboardCopy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
523
frontend/app/documentation/components/HttpLabForm.tsx
Normal file
523
frontend/app/documentation/components/HttpLabForm.tsx
Normal file
@@ -0,0 +1,523 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/documentation/components/HttpLabForm.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Send, Paperclip, X, FileText, Image, File, RotateCcw, Trash2 } from "lucide-react";
|
||||||
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
interface AttachedFile {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
size: number;
|
||||||
|
base64: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HttpLabFormProps {
|
||||||
|
agentUrl: string;
|
||||||
|
setAgentUrl: (url: string) => void;
|
||||||
|
apiKey: string;
|
||||||
|
setApiKey: (key: string) => void;
|
||||||
|
message: string;
|
||||||
|
setMessage: (message: string) => void;
|
||||||
|
sessionId: string;
|
||||||
|
setSessionId: (id: string) => void;
|
||||||
|
taskId: string;
|
||||||
|
setTaskId: (id: string) => void;
|
||||||
|
callId: string;
|
||||||
|
setCallId: (id: string) => void;
|
||||||
|
sendRequest: () => Promise<void>;
|
||||||
|
isLoading: boolean;
|
||||||
|
setFiles?: (files: AttachedFile[]) => void;
|
||||||
|
a2aMethod: string;
|
||||||
|
setA2aMethod: (method: string) => void;
|
||||||
|
authMethod: string;
|
||||||
|
setAuthMethod: (method: string) => void;
|
||||||
|
generateNewIds: () => void;
|
||||||
|
currentTaskId?: string | null;
|
||||||
|
conversationHistory?: any[];
|
||||||
|
clearHistory?: () => void;
|
||||||
|
webhookUrl?: string;
|
||||||
|
setWebhookUrl?: (url: string) => void;
|
||||||
|
enableWebhooks?: boolean;
|
||||||
|
setEnableWebhooks?: (enabled: boolean) => void;
|
||||||
|
showDetailedErrors?: boolean;
|
||||||
|
setShowDetailedErrors?: (show: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HttpLabForm({
|
||||||
|
agentUrl,
|
||||||
|
setAgentUrl,
|
||||||
|
apiKey,
|
||||||
|
setApiKey,
|
||||||
|
message,
|
||||||
|
setMessage,
|
||||||
|
sessionId,
|
||||||
|
setSessionId,
|
||||||
|
taskId,
|
||||||
|
setTaskId,
|
||||||
|
callId,
|
||||||
|
setCallId,
|
||||||
|
sendRequest,
|
||||||
|
isLoading,
|
||||||
|
setFiles = () => {},
|
||||||
|
a2aMethod,
|
||||||
|
setA2aMethod,
|
||||||
|
authMethod,
|
||||||
|
setAuthMethod,
|
||||||
|
generateNewIds,
|
||||||
|
currentTaskId,
|
||||||
|
conversationHistory,
|
||||||
|
clearHistory,
|
||||||
|
webhookUrl,
|
||||||
|
setWebhookUrl,
|
||||||
|
enableWebhooks,
|
||||||
|
setEnableWebhooks,
|
||||||
|
showDetailedErrors,
|
||||||
|
setShowDetailedErrors
|
||||||
|
}: HttpLabFormProps) {
|
||||||
|
const [attachedFiles, setAttachedFiles] = useState<AttachedFile[]>([]);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const clearAttachedFiles = () => {
|
||||||
|
setAttachedFiles([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendRequest = async () => {
|
||||||
|
await sendRequest();
|
||||||
|
clearAttachedFiles();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (!e.target.files || e.target.files.length === 0) return;
|
||||||
|
|
||||||
|
const maxFileSize = 5 * 1024 * 1024; // 5MB limit
|
||||||
|
const newFiles = Array.from(e.target.files);
|
||||||
|
|
||||||
|
if (attachedFiles.length + newFiles.length > 5) {
|
||||||
|
toast({
|
||||||
|
title: "File limit exceeded",
|
||||||
|
description: "You can only attach up to 5 files.",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filesToAdd: AttachedFile[] = [];
|
||||||
|
|
||||||
|
for (const file of newFiles) {
|
||||||
|
if (file.size > maxFileSize) {
|
||||||
|
toast({
|
||||||
|
title: "File too large",
|
||||||
|
description: `The file ${file.name} exceeds the 5MB size limit.`,
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const base64 = await readFileAsBase64(file);
|
||||||
|
filesToAdd.push({
|
||||||
|
name: file.name,
|
||||||
|
type: file.type,
|
||||||
|
size: file.size,
|
||||||
|
base64: base64
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to read file:", error);
|
||||||
|
toast({
|
||||||
|
title: "Failed to read file",
|
||||||
|
description: `Could not process ${file.name}.`,
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filesToAdd.length > 0) {
|
||||||
|
const updatedFiles = [...attachedFiles, ...filesToAdd];
|
||||||
|
setAttachedFiles(updatedFiles);
|
||||||
|
setFiles(updatedFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset file input
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const readFileAsBase64 = (file: File): Promise<string> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const result = reader.result as string;
|
||||||
|
const base64 = result.split(',')[1]; // Remove data URL prefix
|
||||||
|
resolve(base64);
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFile = (index: number) => {
|
||||||
|
const updatedFiles = attachedFiles.filter((_, i) => i !== index);
|
||||||
|
setAttachedFiles(updatedFiles);
|
||||||
|
setFiles(updatedFiles);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatFileSize = (bytes: number): string => {
|
||||||
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isImageFile = (type: string): boolean => {
|
||||||
|
return type.startsWith('image/');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* A2A Method and Authentication Settings */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-[#1a1a1a] border border-[#333] rounded-md">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-neutral-400 mb-2 block">A2A Method</label>
|
||||||
|
<Select value={a2aMethod} onValueChange={setA2aMethod}>
|
||||||
|
<SelectTrigger className="bg-[#222] border-[#444] text-white">
|
||||||
|
<SelectValue placeholder="Select A2A method" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-[#222] border-[#444]">
|
||||||
|
<SelectItem value="message/send" className="text-white hover:bg-[#333]">
|
||||||
|
message/send
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="message/stream" className="text-white hover:bg-[#333]">
|
||||||
|
message/stream
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="tasks/get" className="text-white hover:bg-[#333]">
|
||||||
|
tasks/get
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="tasks/cancel" className="text-white hover:bg-[#333]">
|
||||||
|
tasks/cancel
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="tasks/pushNotificationConfig/set" className="text-white hover:bg-[#333]">
|
||||||
|
tasks/pushNotificationConfig/set
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="tasks/pushNotificationConfig/get" className="text-white hover:bg-[#333]">
|
||||||
|
tasks/pushNotificationConfig/get
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="tasks/resubscribe" className="text-white hover:bg-[#333]">
|
||||||
|
tasks/resubscribe
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="agent/authenticatedExtendedCard" className="text-white hover:bg-[#333]">
|
||||||
|
agent/authenticatedExtendedCard
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-neutral-400 mb-2 block">Authentication Method</label>
|
||||||
|
<Select value={authMethod} onValueChange={setAuthMethod}>
|
||||||
|
<SelectTrigger className="bg-[#222] border-[#444] text-white">
|
||||||
|
<SelectValue placeholder="Select auth method" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-[#222] border-[#444]">
|
||||||
|
<SelectItem value="api-key" className="text-white hover:bg-[#333]">
|
||||||
|
API Key (x-api-key header)
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="bearer" className="text-white hover:bg-[#333]">
|
||||||
|
Bearer Token (Authorization header)
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Multi-turn Conversation History Controls */}
|
||||||
|
{(a2aMethod === "message/send" || a2aMethod === "message/stream") && conversationHistory && conversationHistory.length > 0 && (
|
||||||
|
<div className="p-4 bg-emerald-500/5 border border-emerald-500/20 rounded-md">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-sm font-medium text-emerald-400">
|
||||||
|
Multi-turn Conversation Active
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-emerald-300">
|
||||||
|
💬 {conversationHistory.length} messages in conversation history (contextId active)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Push Notifications (Webhook) Configuration */}
|
||||||
|
{(a2aMethod === "message/send" || a2aMethod === "message/stream" || a2aMethod.startsWith("tasks/")) && (
|
||||||
|
<div className="p-4 bg-blue-500/5 border border-blue-500/20 rounded-md">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="enableWebhooks"
|
||||||
|
checked={enableWebhooks}
|
||||||
|
onChange={(e) => setEnableWebhooks?.(e.target.checked)}
|
||||||
|
className="rounded bg-[#222] border-[#444] text-blue-400 focus:ring-blue-400"
|
||||||
|
/>
|
||||||
|
<label htmlFor="enableWebhooks" className="text-sm font-medium text-blue-400">
|
||||||
|
Enable Push Notifications (Webhooks)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{enableWebhooks && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<label className="text-sm text-neutral-400 mb-1 block">Webhook URL</label>
|
||||||
|
<Input
|
||||||
|
value={webhookUrl}
|
||||||
|
onChange={(e) => setWebhookUrl?.(e.target.value)}
|
||||||
|
placeholder="https://your-server.com/webhook/a2a"
|
||||||
|
className="bg-[#222] border-[#444] text-white"
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-blue-300 mt-1">
|
||||||
|
{a2aMethod === "tasks/pushNotificationConfig/set"
|
||||||
|
? "📡 Configure push notifications for task"
|
||||||
|
: "📡 Webhook URL for push notifications (configured via pushNotificationConfig)"
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!enableWebhooks && (
|
||||||
|
<div className="text-xs text-neutral-400">
|
||||||
|
{a2aMethod === "tasks/pushNotificationConfig/set"
|
||||||
|
? "Push notification configuration will be set to null."
|
||||||
|
: "No push notifications will be configured for this request."
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Advanced Error Handling Configuration */}
|
||||||
|
<div className="p-4 bg-orange-500/5 border border-orange-500/20 rounded-md">
|
||||||
|
<div className="flex items-center space-x-2 mb-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="showDetailedErrors"
|
||||||
|
checked={showDetailedErrors}
|
||||||
|
onChange={(e) => setShowDetailedErrors?.(e.target.checked)}
|
||||||
|
className="rounded bg-[#222] border-[#444] text-orange-400 focus:ring-orange-400"
|
||||||
|
/>
|
||||||
|
<label htmlFor="showDetailedErrors" className="text-sm font-medium text-orange-400">
|
||||||
|
Enable Detailed Error Logging
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-neutral-400">
|
||||||
|
{showDetailedErrors
|
||||||
|
? "🔍 Detailed error information will be shown in debug logs (client-side only)."
|
||||||
|
: "⚡ Basic error handling only - minimal error information in logs."
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-neutral-400 mb-1 block">Agent URL</label>
|
||||||
|
<Input
|
||||||
|
value={agentUrl}
|
||||||
|
onChange={(e) => setAgentUrl(e.target.value)}
|
||||||
|
placeholder="http://localhost:8000/api/v1/a2a/your-agent-id"
|
||||||
|
className="bg-[#222] border-[#444] text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-neutral-400 mb-1 block">
|
||||||
|
{authMethod === "bearer" ? "Bearer Token" : "API Key"} (optional)
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={apiKey}
|
||||||
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
|
placeholder={authMethod === "bearer" ? "Your Bearer token" : "Your API key"}
|
||||||
|
className="bg-[#222] border-[#444] text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Show current task ID if available */}
|
||||||
|
{currentTaskId && (
|
||||||
|
<div className="p-3 bg-[#1a1a1a] border border-emerald-400/20 rounded-md">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<span className="text-sm text-neutral-400">Current Task ID:</span>
|
||||||
|
<span className="ml-2 text-emerald-400 font-mono text-sm">{currentTaskId}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Message input - only show for message methods */}
|
||||||
|
{(a2aMethod === "message/send" || a2aMethod === "message/stream") && (
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-neutral-400 mb-1 block">Message</label>
|
||||||
|
<Textarea
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
placeholder="What is the A2A protocol?"
|
||||||
|
className="bg-[#222] border-[#444] text-white min-h-[100px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* File attachment - only show for message methods */}
|
||||||
|
{(a2aMethod === "message/send" || a2aMethod === "message/stream") && (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<label className="text-sm text-neutral-400">
|
||||||
|
Attach Files (up to 5, max 5MB each)
|
||||||
|
</label>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={attachedFiles.length >= 5}
|
||||||
|
>
|
||||||
|
<Paperclip className="h-4 w-4 mr-2" />
|
||||||
|
Browse Files
|
||||||
|
</Button>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
className="hidden"
|
||||||
|
multiple
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{attachedFiles.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 mb-4">
|
||||||
|
{attachedFiles.map((file, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center gap-1.5 bg-[#333] text-white rounded-md p-1.5 text-xs"
|
||||||
|
>
|
||||||
|
{isImageFile(file.type) ? (
|
||||||
|
<Image className="h-4 w-4 text-emerald-400" />
|
||||||
|
) : file.type === 'application/pdf' ? (
|
||||||
|
<FileText className="h-4 w-4 text-emerald-400" />
|
||||||
|
) : (
|
||||||
|
<File className="h-4 w-4 text-emerald-400" />
|
||||||
|
)}
|
||||||
|
<span className="max-w-[150px] truncate">{file.name}</span>
|
||||||
|
<span className="text-neutral-400">({formatFileSize(file.size)})</span>
|
||||||
|
<button
|
||||||
|
onClick={() => removeFile(index)}
|
||||||
|
className="ml-1 text-neutral-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Separator className="my-4 bg-[#333]" />
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<label className="text-sm text-neutral-400">Session ID</label>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={generateNewIds}
|
||||||
|
className="h-6 px-2 text-xs text-neutral-400 hover:text-white"
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-3 w-3 mr-1" />
|
||||||
|
New IDs
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={sessionId}
|
||||||
|
onChange={(e) => setSessionId(e.target.value)}
|
||||||
|
className="bg-[#222] border-[#444] text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-neutral-400 mb-1 block">
|
||||||
|
{a2aMethod.startsWith("tasks/") ? "Task ID (for operation)" : "Message ID (UUID)"}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={taskId}
|
||||||
|
onChange={(e) => setTaskId(e.target.value)}
|
||||||
|
className="bg-[#222] border-[#444] text-white"
|
||||||
|
placeholder={a2aMethod.startsWith("tasks/") ? "Task ID to query/cancel" : "UUID for message"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-neutral-400 mb-1 block">Request ID</label>
|
||||||
|
<Input
|
||||||
|
value={callId}
|
||||||
|
onChange={(e) => setCallId(e.target.value)}
|
||||||
|
className="bg-[#222] border-[#444] text-white"
|
||||||
|
placeholder="req-123"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleSendRequest}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="bg-emerald-400 text-black hover:bg-[#00cc7d] w-full mt-4"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-black mr-2"></div>
|
||||||
|
Sending...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Send className="mr-2 h-4 w-4" />
|
||||||
|
{a2aMethod === "message/send" && "Send Message"}
|
||||||
|
{a2aMethod === "message/stream" && "Start Stream"}
|
||||||
|
{a2aMethod === "tasks/get" && "Get Task Status"}
|
||||||
|
{a2aMethod === "tasks/cancel" && "Cancel Task"}
|
||||||
|
{a2aMethod === "tasks/pushNotificationConfig/set" && "Set Push Config"}
|
||||||
|
{a2aMethod === "tasks/pushNotificationConfig/get" && "Get Push Config"}
|
||||||
|
{a2aMethod === "tasks/resubscribe" && "Resubscribe to Task"}
|
||||||
|
{a2aMethod === "agent/authenticatedExtendedCard" && "Get Agent Card"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
201
frontend/app/documentation/components/LabSection.tsx
Normal file
201
frontend/app/documentation/components/LabSection.tsx
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/documentation/components/LabSection.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ClipboardCopy } from "lucide-react";
|
||||||
|
import { HttpLabForm } from "@/app/documentation/components/HttpLabForm";
|
||||||
|
import { StreamLabForm } from "@/app/documentation/components/StreamLabForm";
|
||||||
|
import { CodeBlock } from "@/app/documentation/components/CodeBlock";
|
||||||
|
|
||||||
|
interface LabSectionProps {
|
||||||
|
agentUrl: string;
|
||||||
|
setAgentUrl: (url: string) => void;
|
||||||
|
apiKey: string;
|
||||||
|
setApiKey: (key: string) => void;
|
||||||
|
message: string;
|
||||||
|
setMessage: (message: string) => void;
|
||||||
|
sessionId: string;
|
||||||
|
setSessionId: (id: string) => void;
|
||||||
|
taskId: string;
|
||||||
|
setTaskId: (id: string) => void;
|
||||||
|
callId: string;
|
||||||
|
setCallId: (id: string) => void;
|
||||||
|
a2aMethod: string;
|
||||||
|
setA2aMethod: (method: string) => void;
|
||||||
|
authMethod: string;
|
||||||
|
setAuthMethod: (method: string) => void;
|
||||||
|
generateNewIds: () => void;
|
||||||
|
sendRequest: () => Promise<void>;
|
||||||
|
sendStreamRequestWithEventSource: () => Promise<void>;
|
||||||
|
isLoading: boolean;
|
||||||
|
isStreaming: boolean;
|
||||||
|
streamResponse: string;
|
||||||
|
streamStatus: string;
|
||||||
|
streamHistory: string[];
|
||||||
|
streamComplete: boolean;
|
||||||
|
response: string;
|
||||||
|
copyToClipboard: (text: string) => void;
|
||||||
|
renderStatusIndicator: () => JSX.Element | null;
|
||||||
|
renderTypingIndicator: () => JSX.Element | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LabSection({
|
||||||
|
agentUrl,
|
||||||
|
setAgentUrl,
|
||||||
|
apiKey,
|
||||||
|
setApiKey,
|
||||||
|
message,
|
||||||
|
setMessage,
|
||||||
|
sessionId,
|
||||||
|
setSessionId,
|
||||||
|
taskId,
|
||||||
|
setTaskId,
|
||||||
|
callId,
|
||||||
|
setCallId,
|
||||||
|
a2aMethod,
|
||||||
|
setA2aMethod,
|
||||||
|
authMethod,
|
||||||
|
setAuthMethod,
|
||||||
|
generateNewIds,
|
||||||
|
sendRequest,
|
||||||
|
sendStreamRequestWithEventSource,
|
||||||
|
isLoading,
|
||||||
|
isStreaming,
|
||||||
|
streamResponse,
|
||||||
|
streamStatus,
|
||||||
|
streamHistory,
|
||||||
|
streamComplete,
|
||||||
|
response,
|
||||||
|
copyToClipboard,
|
||||||
|
renderStatusIndicator,
|
||||||
|
renderTypingIndicator
|
||||||
|
}: LabSectionProps) {
|
||||||
|
const [labMode, setLabMode] = useState("http");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card className="bg-[#1a1a1a] border-[#333] text-white mb-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-emerald-400">A2A Test Lab</CardTitle>
|
||||||
|
<CardDescription className="text-neutral-400">
|
||||||
|
Test your A2A agent with different communication methods
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Tabs defaultValue="http" onValueChange={setLabMode}>
|
||||||
|
<TabsList className="bg-[#222] border-[#333] mb-4">
|
||||||
|
<TabsTrigger value="http" className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400">
|
||||||
|
HTTP Request
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="stream" className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400">
|
||||||
|
Streaming
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="http">
|
||||||
|
<HttpLabForm
|
||||||
|
agentUrl={agentUrl}
|
||||||
|
setAgentUrl={setAgentUrl}
|
||||||
|
apiKey={apiKey}
|
||||||
|
setApiKey={setApiKey}
|
||||||
|
message={message}
|
||||||
|
setMessage={setMessage}
|
||||||
|
sessionId={sessionId}
|
||||||
|
setSessionId={setSessionId}
|
||||||
|
taskId={taskId}
|
||||||
|
setTaskId={setTaskId}
|
||||||
|
callId={callId}
|
||||||
|
setCallId={setCallId}
|
||||||
|
a2aMethod={a2aMethod}
|
||||||
|
setA2aMethod={setA2aMethod}
|
||||||
|
authMethod={authMethod}
|
||||||
|
setAuthMethod={setAuthMethod}
|
||||||
|
generateNewIds={generateNewIds}
|
||||||
|
sendRequest={sendRequest}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="stream">
|
||||||
|
<StreamLabForm
|
||||||
|
agentUrl={agentUrl}
|
||||||
|
setAgentUrl={setAgentUrl}
|
||||||
|
apiKey={apiKey}
|
||||||
|
setApiKey={setApiKey}
|
||||||
|
message={message}
|
||||||
|
setMessage={setMessage}
|
||||||
|
sessionId={sessionId}
|
||||||
|
setSessionId={setSessionId}
|
||||||
|
taskId={taskId}
|
||||||
|
setTaskId={setTaskId}
|
||||||
|
callId={callId}
|
||||||
|
setCallId={setCallId}
|
||||||
|
authMethod={authMethod}
|
||||||
|
sendStreamRequest={sendStreamRequestWithEventSource}
|
||||||
|
isStreaming={isStreaming}
|
||||||
|
streamResponse={streamResponse}
|
||||||
|
streamStatus={streamStatus}
|
||||||
|
streamHistory={streamHistory}
|
||||||
|
renderStatusIndicator={renderStatusIndicator}
|
||||||
|
renderTypingIndicator={renderTypingIndicator}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{response && labMode === "http" && (
|
||||||
|
<Card className="bg-[#1a1a1a] border-[#333] text-white">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-emerald-400">Response</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="relative">
|
||||||
|
<CodeBlock
|
||||||
|
text={response}
|
||||||
|
language="json"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||||
|
onClick={() => copyToClipboard(response)}
|
||||||
|
>
|
||||||
|
<ClipboardCopy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
179
frontend/app/documentation/components/QuickStartTemplates.tsx
Normal file
179
frontend/app/documentation/components/QuickStartTemplates.tsx
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/documentation/components/QuickStartTemplates.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
MessageSquare,
|
||||||
|
FileText,
|
||||||
|
Zap,
|
||||||
|
Settings,
|
||||||
|
Users,
|
||||||
|
Play
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
interface QuickStartTemplate {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
icon: any;
|
||||||
|
method: string;
|
||||||
|
message: string;
|
||||||
|
useCase: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QuickStartTemplatesProps {
|
||||||
|
onSelectTemplate: (template: QuickStartTemplate) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QuickStartTemplates({ onSelectTemplate }: QuickStartTemplatesProps) {
|
||||||
|
const templates: QuickStartTemplate[] = [
|
||||||
|
{
|
||||||
|
id: "hello",
|
||||||
|
name: "Hello Agent",
|
||||||
|
description: "Simple greeting to test agent connectivity",
|
||||||
|
icon: MessageSquare,
|
||||||
|
method: "message/send",
|
||||||
|
message: "Hello! Can you introduce yourself and tell me what you can do?",
|
||||||
|
useCase: "Basic connectivity test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "analysis",
|
||||||
|
name: "Data Analysis",
|
||||||
|
description: "Request data analysis and insights",
|
||||||
|
icon: FileText,
|
||||||
|
method: "message/send",
|
||||||
|
message: "Please analyze the current market trends in AI technology and provide key insights with recommendations.",
|
||||||
|
useCase: "Complex analytical tasks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "streaming",
|
||||||
|
name: "Long Content",
|
||||||
|
description: "Generate lengthy content with streaming",
|
||||||
|
icon: Zap,
|
||||||
|
method: "message/stream",
|
||||||
|
message: "Write a comprehensive guide about implementing the Agent2Agent protocol, including technical details, best practices, and code examples.",
|
||||||
|
useCase: "Streaming responses"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "task-query",
|
||||||
|
name: "Task Status",
|
||||||
|
description: "Query the status of a running task",
|
||||||
|
icon: Settings,
|
||||||
|
method: "tasks/get",
|
||||||
|
message: "",
|
||||||
|
useCase: "Task management"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "capabilities",
|
||||||
|
name: "Agent Capabilities",
|
||||||
|
description: "Discover agent capabilities and skills",
|
||||||
|
icon: Users,
|
||||||
|
method: "agent/authenticatedExtendedCard",
|
||||||
|
message: "",
|
||||||
|
useCase: "Agent discovery"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const getMethodColor = (method: string) => {
|
||||||
|
switch (method) {
|
||||||
|
case 'message/send': return 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30';
|
||||||
|
case 'message/stream': return 'bg-blue-500/20 text-blue-400 border-blue-500/30';
|
||||||
|
case 'tasks/get': return 'bg-purple-500/20 text-purple-400 border-purple-500/30';
|
||||||
|
case 'tasks/cancel': return 'bg-red-500/20 text-red-400 border-red-500/30';
|
||||||
|
case 'agent/authenticatedExtendedCard': return 'bg-orange-500/20 text-orange-400 border-orange-500/30';
|
||||||
|
default: return 'bg-neutral-500/20 text-neutral-400 border-neutral-500/30';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-[#1a1a1a] border-[#333] text-white mb-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-emerald-400 flex items-center">
|
||||||
|
<Play className="h-5 w-5 mr-2" />
|
||||||
|
Quick Start Templates
|
||||||
|
</CardTitle>
|
||||||
|
<p className="text-neutral-400 text-sm">
|
||||||
|
Choose a template to quickly test different A2A protocol methods
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{templates.map((template) => {
|
||||||
|
const IconComponent = template.icon;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={template.id}
|
||||||
|
className="bg-[#222] border border-[#444] rounded-lg p-4 hover:border-emerald-500/50 transition-colors cursor-pointer group"
|
||||||
|
onClick={() => onSelectTemplate(template)}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="bg-emerald-500/20 p-2 rounded-lg">
|
||||||
|
<IconComponent className="h-4 w-4 text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-white text-sm">{template.name}</h3>
|
||||||
|
<p className="text-xs text-neutral-400">{template.useCase}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-neutral-300 mb-3 line-clamp-2">
|
||||||
|
{template.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Badge className={`text-xs ${getMethodColor(template.method)}`}>
|
||||||
|
{template.method}
|
||||||
|
</Badge>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/10 text-xs px-2 py-1 h-auto opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
>
|
||||||
|
Use Template
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg">
|
||||||
|
<p className="text-blue-300 text-xs">
|
||||||
|
💡 <strong>Tip:</strong> These templates automatically configure the correct A2A method and provide example messages.
|
||||||
|
Simply select one and customize the agent URL and authentication.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
366
frontend/app/documentation/components/StreamLabForm.tsx
Normal file
366
frontend/app/documentation/components/StreamLabForm.tsx
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/documentation/components/StreamLabForm.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Send, Paperclip, X, FileText, Image, File } from "lucide-react";
|
||||||
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
interface AttachedFile {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
size: number;
|
||||||
|
base64: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StreamLabFormProps {
|
||||||
|
agentUrl: string;
|
||||||
|
setAgentUrl: (url: string) => void;
|
||||||
|
apiKey: string;
|
||||||
|
setApiKey: (key: string) => void;
|
||||||
|
message: string;
|
||||||
|
setMessage: (message: string) => void;
|
||||||
|
sessionId: string;
|
||||||
|
setSessionId: (id: string) => void;
|
||||||
|
taskId: string;
|
||||||
|
setTaskId: (id: string) => void;
|
||||||
|
callId: string;
|
||||||
|
setCallId: (id: string) => void;
|
||||||
|
sendStreamRequest: () => Promise<void>;
|
||||||
|
isStreaming: boolean;
|
||||||
|
streamResponse: string;
|
||||||
|
streamStatus: string;
|
||||||
|
streamHistory: string[];
|
||||||
|
renderStatusIndicator: () => JSX.Element | null;
|
||||||
|
renderTypingIndicator: () => JSX.Element | null;
|
||||||
|
setFiles?: (files: AttachedFile[]) => void;
|
||||||
|
authMethod: string;
|
||||||
|
currentTaskId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StreamLabForm({
|
||||||
|
agentUrl,
|
||||||
|
setAgentUrl,
|
||||||
|
apiKey,
|
||||||
|
setApiKey,
|
||||||
|
message,
|
||||||
|
setMessage,
|
||||||
|
sessionId,
|
||||||
|
setSessionId,
|
||||||
|
taskId,
|
||||||
|
setTaskId,
|
||||||
|
callId,
|
||||||
|
setCallId,
|
||||||
|
sendStreamRequest,
|
||||||
|
isStreaming,
|
||||||
|
streamResponse,
|
||||||
|
streamStatus,
|
||||||
|
streamHistory,
|
||||||
|
renderStatusIndicator,
|
||||||
|
renderTypingIndicator,
|
||||||
|
setFiles = () => {},
|
||||||
|
authMethod,
|
||||||
|
currentTaskId
|
||||||
|
}: StreamLabFormProps) {
|
||||||
|
const [attachedFiles, setAttachedFiles] = useState<AttachedFile[]>([]);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const clearAttachedFiles = () => {
|
||||||
|
setAttachedFiles([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendStreamRequest = async () => {
|
||||||
|
await sendStreamRequest();
|
||||||
|
clearAttachedFiles();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (!e.target.files || e.target.files.length === 0) return;
|
||||||
|
|
||||||
|
const maxFileSize = 5 * 1024 * 1024; // 5MB limit
|
||||||
|
const newFiles = Array.from(e.target.files);
|
||||||
|
|
||||||
|
if (attachedFiles.length + newFiles.length > 5) {
|
||||||
|
toast({
|
||||||
|
title: "File limit exceeded",
|
||||||
|
description: "You can only attach up to 5 files.",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filesToAdd: AttachedFile[] = [];
|
||||||
|
|
||||||
|
for (const file of newFiles) {
|
||||||
|
if (file.size > maxFileSize) {
|
||||||
|
toast({
|
||||||
|
title: "File too large",
|
||||||
|
description: `The file ${file.name} exceeds the 5MB size limit.`,
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const base64 = await readFileAsBase64(file);
|
||||||
|
filesToAdd.push({
|
||||||
|
name: file.name,
|
||||||
|
type: file.type,
|
||||||
|
size: file.size,
|
||||||
|
base64: base64
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to read file:", error);
|
||||||
|
toast({
|
||||||
|
title: "Failed to read file",
|
||||||
|
description: `Could not process ${file.name}.`,
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filesToAdd.length > 0) {
|
||||||
|
const updatedFiles = [...attachedFiles, ...filesToAdd];
|
||||||
|
setAttachedFiles(updatedFiles);
|
||||||
|
setFiles(updatedFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset file input
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const readFileAsBase64 = (file: File): Promise<string> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const result = reader.result as string;
|
||||||
|
const base64 = result.split(',')[1]; // Remove data URL prefix
|
||||||
|
resolve(base64);
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFile = (index: number) => {
|
||||||
|
const updatedFiles = attachedFiles.filter((_, i) => i !== index);
|
||||||
|
setAttachedFiles(updatedFiles);
|
||||||
|
setFiles(updatedFiles);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatFileSize = (bytes: number): string => {
|
||||||
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isImageFile = (type: string): boolean => {
|
||||||
|
return type.startsWith('image/');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* A2A Streaming Information */}
|
||||||
|
<div className="p-4 bg-[#1a1a1a] border border-[#333] rounded-md">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-sm font-medium text-emerald-400">A2A Streaming Mode</span>
|
||||||
|
<span className="text-xs text-neutral-400">Method: message/stream</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-neutral-400">
|
||||||
|
Authentication: {authMethod === "bearer" ? "Bearer Token" : "API Key"} header
|
||||||
|
</div>
|
||||||
|
{currentTaskId && (
|
||||||
|
<div className="mt-2 pt-2 border-t border-[#333]">
|
||||||
|
<span className="text-xs text-neutral-400">Current Task ID: </span>
|
||||||
|
<span className="text-xs text-emerald-400 font-mono">{currentTaskId}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-neutral-400 mb-1 block">Agent URL</label>
|
||||||
|
<Input
|
||||||
|
value={agentUrl}
|
||||||
|
onChange={(e) => setAgentUrl(e.target.value)}
|
||||||
|
placeholder="http://localhost:8000/api/v1/a2a/your-agent-id"
|
||||||
|
className="bg-[#222] border-[#444] text-white"
|
||||||
|
disabled={isStreaming}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-neutral-400 mb-1 block">
|
||||||
|
{authMethod === "bearer" ? "Bearer Token" : "API Key"} (optional)
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={apiKey}
|
||||||
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
|
placeholder={authMethod === "bearer" ? "Your Bearer token" : "Your API key"}
|
||||||
|
className="bg-[#222] border-[#444] text-white"
|
||||||
|
disabled={isStreaming}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-neutral-400 mb-1 block">Message</label>
|
||||||
|
<Textarea
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
placeholder="What is the A2A protocol?"
|
||||||
|
className="bg-[#222] border-[#444] text-white min-h-[100px]"
|
||||||
|
disabled={isStreaming}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<label className="text-sm text-neutral-400">
|
||||||
|
Attach Files (up to 5, max 5MB each)
|
||||||
|
</label>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={attachedFiles.length >= 5 || isStreaming}
|
||||||
|
>
|
||||||
|
<Paperclip className="h-4 w-4 mr-2" />
|
||||||
|
Browse Files
|
||||||
|
</Button>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
className="hidden"
|
||||||
|
multiple
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
disabled={isStreaming}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{attachedFiles.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 mb-4">
|
||||||
|
{attachedFiles.map((file, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center gap-1.5 bg-[#333] text-white rounded-md p-1.5 text-xs"
|
||||||
|
>
|
||||||
|
{isImageFile(file.type) ? (
|
||||||
|
<Image className="h-4 w-4 text-emerald-400" />
|
||||||
|
) : file.type === 'application/pdf' ? (
|
||||||
|
<FileText className="h-4 w-4 text-emerald-400" />
|
||||||
|
) : (
|
||||||
|
<File className="h-4 w-4 text-emerald-400" />
|
||||||
|
)}
|
||||||
|
<span className="max-w-[150px] truncate">{file.name}</span>
|
||||||
|
<span className="text-neutral-400">({formatFileSize(file.size)})</span>
|
||||||
|
<button
|
||||||
|
onClick={() => removeFile(index)}
|
||||||
|
className="ml-1 text-neutral-400 hover:text-white transition-colors"
|
||||||
|
disabled={isStreaming}
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator className="my-4 bg-[#333]" />
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-neutral-400 mb-1 block">Session ID</label>
|
||||||
|
<Input
|
||||||
|
value={sessionId}
|
||||||
|
onChange={(e) => setSessionId(e.target.value)}
|
||||||
|
className="bg-[#222] border-[#444] text-white"
|
||||||
|
disabled={isStreaming}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-neutral-400 mb-1 block">Task ID</label>
|
||||||
|
<Input
|
||||||
|
value={taskId}
|
||||||
|
onChange={(e) => setTaskId(e.target.value)}
|
||||||
|
className="bg-[#222] border-[#444] text-white"
|
||||||
|
disabled={isStreaming}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-neutral-400 mb-1 block">Call ID</label>
|
||||||
|
<Input
|
||||||
|
value={callId}
|
||||||
|
onChange={(e) => setCallId(e.target.value)}
|
||||||
|
className="bg-[#222] border-[#444] text-white"
|
||||||
|
disabled={isStreaming}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleSendStreamRequest}
|
||||||
|
disabled={isStreaming}
|
||||||
|
className="bg-emerald-400 text-black hover:bg-[#00cc7d] w-full mt-4"
|
||||||
|
>
|
||||||
|
{isStreaming ? (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-black mr-2"></div>
|
||||||
|
Streaming...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Send className="mr-2 h-4 w-4" />
|
||||||
|
Start Streaming
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{streamResponse && (
|
||||||
|
<div className="mt-6 rounded-md bg-[#222] border border-[#333] p-4">
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<h3 className="text-lg font-medium text-white">Response</h3>
|
||||||
|
{renderStatusIndicator && renderStatusIndicator()}
|
||||||
|
</div>
|
||||||
|
<div className="whitespace-pre-wrap text-sm font-mono text-neutral-300">
|
||||||
|
{streamResponse}
|
||||||
|
</div>
|
||||||
|
{renderTypingIndicator && renderTypingIndicator()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,470 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/documentation/components/TechnicalDetailsSection.tsx │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ClipboardCopy } from "lucide-react";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { CodeBlock } from "@/app/documentation/components/CodeBlock";
|
||||||
|
|
||||||
|
interface TechnicalDetailsSectionProps {
|
||||||
|
copyToClipboard: (text: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TechnicalDetailsSection({ copyToClipboard }: TechnicalDetailsSectionProps) {
|
||||||
|
return (
|
||||||
|
<Card className="bg-[#1a1a1a] border-[#333] text-white">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-emerald-400">Technical Details of the Methods</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-emerald-400 text-lg font-medium mb-2">Method message/send</h3>
|
||||||
|
<p className="text-neutral-300 mb-4">
|
||||||
|
The <code className="bg-[#333] px-1 rounded">message/send</code> method performs a standard HTTP request and waits for the complete response.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-white mb-2">Request:</h4>
|
||||||
|
<div className="relative">
|
||||||
|
<CodeBlock
|
||||||
|
text={JSON.stringify({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: "call-123",
|
||||||
|
method: "message/send",
|
||||||
|
params: {
|
||||||
|
id: "task-456",
|
||||||
|
sessionId: "session-789",
|
||||||
|
message: {
|
||||||
|
role: "user",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Your question here"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, null, 2)}
|
||||||
|
language="json"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||||
|
onClick={() => copyToClipboard(JSON.stringify({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: "call-123",
|
||||||
|
method: "message/send",
|
||||||
|
params: {
|
||||||
|
id: "task-456",
|
||||||
|
sessionId: "session-789",
|
||||||
|
message: {
|
||||||
|
role: "user",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Your question here"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, null, 2))}
|
||||||
|
>
|
||||||
|
<ClipboardCopy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-white mb-2">Headers:</h4>
|
||||||
|
<div className="relative">
|
||||||
|
<CodeBlock
|
||||||
|
text={`Content-Type: application/json
|
||||||
|
x-api-key: YOUR_API_KEY`}
|
||||||
|
language="text"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||||
|
onClick={() => copyToClipboard(`Content-Type: application/json
|
||||||
|
x-api-key: YOUR_API_KEY`)}
|
||||||
|
>
|
||||||
|
<ClipboardCopy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-white mb-2">Response:</h4>
|
||||||
|
<div className="relative">
|
||||||
|
<CodeBlock
|
||||||
|
text={JSON.stringify({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
result: {
|
||||||
|
status: {
|
||||||
|
state: "completed",
|
||||||
|
message: {
|
||||||
|
role: "model",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Complete agent response here."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
id: "call-123"
|
||||||
|
}, null, 2)}
|
||||||
|
language="json"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||||
|
onClick={() => copyToClipboard(JSON.stringify({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
result: {
|
||||||
|
status: {
|
||||||
|
state: "completed",
|
||||||
|
message: {
|
||||||
|
role: "model",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Complete agent response here."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
id: "call-123"
|
||||||
|
}, null, 2))}
|
||||||
|
>
|
||||||
|
<ClipboardCopy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator className="my-6 bg-[#333]" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-emerald-400 text-lg font-medium mb-2">Method message/stream</h3>
|
||||||
|
<p className="text-neutral-300 mb-4">
|
||||||
|
The <code className="bg-[#333] px-1 rounded">message/stream</code> method uses Server-Sent Events (SSE) to receive real-time updates.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-white mb-2">Request:</h4>
|
||||||
|
<div className="relative">
|
||||||
|
<CodeBlock
|
||||||
|
text={JSON.stringify({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: "call-123",
|
||||||
|
method: "message/stream",
|
||||||
|
params: {
|
||||||
|
id: "task-456",
|
||||||
|
sessionId: "session-789",
|
||||||
|
message: {
|
||||||
|
role: "user",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Your question here"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, null, 2)}
|
||||||
|
language="json"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||||
|
onClick={() => copyToClipboard(JSON.stringify({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: "call-123",
|
||||||
|
method: "message/stream",
|
||||||
|
params: {
|
||||||
|
id: "task-456",
|
||||||
|
sessionId: "session-789",
|
||||||
|
message: {
|
||||||
|
role: "user",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Your question here"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, null, 2))}
|
||||||
|
>
|
||||||
|
<ClipboardCopy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-white mb-2">Headers:</h4>
|
||||||
|
<div className="relative">
|
||||||
|
<CodeBlock
|
||||||
|
text={`Content-Type: application/json
|
||||||
|
x-api-key: YOUR_API_KEY
|
||||||
|
Accept: text/event-stream`}
|
||||||
|
language="text"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||||
|
onClick={() => copyToClipboard(`Content-Type: application/json
|
||||||
|
x-api-key: YOUR_API_KEY
|
||||||
|
Accept: text/event-stream`)}
|
||||||
|
>
|
||||||
|
<ClipboardCopy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-white mb-2">SSE Event Format:</h4>
|
||||||
|
<p className="text-neutral-300 mb-4">
|
||||||
|
Each event follows the standard Server-Sent Events (SSE) format, with the "data:" prefix followed by the JSON content and terminated by two newlines ("\n\n"):
|
||||||
|
</p>
|
||||||
|
<div className="relative">
|
||||||
|
<CodeBlock
|
||||||
|
text={`data: {"jsonrpc":"2.0","id":"call-123","result":{"id":"task-456","status":{"state":"working","message":{"role":"agent","parts":[{"type":"text","text":"Processing..."}]},"timestamp":"2025-05-13T18:10:37.219Z"},"final":false}}
|
||||||
|
|
||||||
|
data: {"jsonrpc":"2.0","id":"call-123","result":{"id":"task-456","status":{"state":"completed","timestamp":"2025-05-13T18:10:40.456Z"},"final":true}}
|
||||||
|
`}
|
||||||
|
language="text"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||||
|
onClick={() => copyToClipboard(`data: {"jsonrpc":"2.0","id":"call-123","result":{"id":"task-456","status":{"state":"working","message":{"role":"agent","parts":[{"type":"text","text":"Processing..."}]},"timestamp":"2025-05-13T18:10:37.219Z"},"final":false}}
|
||||||
|
|
||||||
|
data: {"jsonrpc":"2.0","id":"call-123","result":{"id":"task-456","status":{"state":"completed","timestamp":"2025-05-13T18:10:40.456Z"},"final":true}}
|
||||||
|
`)}
|
||||||
|
>
|
||||||
|
<ClipboardCopy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-white mb-2">Event Types:</h4>
|
||||||
|
<ul className="list-disc list-inside text-neutral-300 space-y-2 mb-4">
|
||||||
|
<li><span className="text-emerald-400">Status Updates</span>: Contains the <code className="bg-[#333] px-1 rounded">status</code> field with information about the task status.</li>
|
||||||
|
<li><span className="text-emerald-400">Artifact Updates</span>: Contains the <code className="bg-[#333] px-1 rounded">artifact</code> field with the content generated by the agent.</li>
|
||||||
|
<li><span className="text-emerald-400">Ping Events</span>: Simple events with the format <code className="bg-[#333] px-1 rounded">: ping</code> to keep the connection active.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-white mb-2">Client Consumption:</h4>
|
||||||
|
<p className="text-neutral-300 mb-2">
|
||||||
|
For a better experience, we recommend using the <code className="bg-[#333] px-1 rounded">EventSource</code> API to consume the events:
|
||||||
|
</p>
|
||||||
|
<div className="relative">
|
||||||
|
<CodeBlock
|
||||||
|
text={`// After receiving the initial response via POST, use EventSource to stream
|
||||||
|
const eventSource = new EventSource(\`/api/v1/a2a/\${agentId}/stream?taskId=\${taskId}&key=\${apiKey}\`);
|
||||||
|
|
||||||
|
// Process the received events
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
// Process different types of events
|
||||||
|
if (data.result) {
|
||||||
|
// 1. Process status updates
|
||||||
|
if (data.result.status) {
|
||||||
|
const state = data.result.status.state; // "working", "completed", etc.
|
||||||
|
|
||||||
|
// Check if there is a text message
|
||||||
|
if (data.result.status.message?.parts) {
|
||||||
|
const textParts = data.result.status.message.parts
|
||||||
|
.filter(part => part.type === "text")
|
||||||
|
.map(part => part.text)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
// Update UI with the text
|
||||||
|
updateUI(textParts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it is the final event
|
||||||
|
if (data.result.final === true) {
|
||||||
|
eventSource.close(); // Close connection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Process the generated artifacts
|
||||||
|
if (data.result.artifact) {
|
||||||
|
const artifact = data.result.artifact;
|
||||||
|
|
||||||
|
// Extract text from the artifact
|
||||||
|
if (artifact.parts) {
|
||||||
|
const artifactText = artifact.parts
|
||||||
|
.filter(part => part.type === "text")
|
||||||
|
.map(part => part.text)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
// Update UI with the artifact
|
||||||
|
updateArtifactUI(artifactText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle errors
|
||||||
|
eventSource.onerror = (error) => {
|
||||||
|
console.error("Error in SSE:", error);
|
||||||
|
eventSource.close();
|
||||||
|
};`}
|
||||||
|
language="javascript"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||||
|
onClick={() => copyToClipboard(`// After receiving the initial response via POST, use EventSource to stream
|
||||||
|
const eventSource = new EventSource(\`/api/v1/a2a/\${agentId}/stream?taskId=\${taskId}&key=\${apiKey}\`);
|
||||||
|
|
||||||
|
// Process the received events
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
// Process different types of events
|
||||||
|
if (data.result) {
|
||||||
|
// 1. Process status updates
|
||||||
|
if (data.result.status) {
|
||||||
|
const state = data.result.status.state; // "working", "completed", etc.
|
||||||
|
|
||||||
|
// Check if there is a text message
|
||||||
|
if (data.result.status.message?.parts) {
|
||||||
|
const textParts = data.result.status.message.parts
|
||||||
|
.filter(part => part.type === "text")
|
||||||
|
.map(part => part.text)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
// Update UI with the text
|
||||||
|
updateUI(textParts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it is the final event
|
||||||
|
if (data.result.final === true) {
|
||||||
|
eventSource.close(); // Close connection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Process the generated artifacts
|
||||||
|
if (data.result.artifact) {
|
||||||
|
const artifact = data.result.artifact;
|
||||||
|
|
||||||
|
// Extract text from the artifact
|
||||||
|
if (artifact.parts) {
|
||||||
|
const artifactText = artifact.parts
|
||||||
|
.filter(part => part.type === "text")
|
||||||
|
.map(part => part.text)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
// Update UI with the artifact
|
||||||
|
updateArtifactUI(artifactText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle errors
|
||||||
|
eventSource.onerror = (error) => {
|
||||||
|
console.error("Error in SSE:", error);
|
||||||
|
eventSource.close();
|
||||||
|
};`)}
|
||||||
|
>
|
||||||
|
<ClipboardCopy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-white mb-2">Possible task states:</h4>
|
||||||
|
<ul className="list-disc list-inside text-neutral-300 space-y-1">
|
||||||
|
<li><span className="text-emerald-400">submitted</span>: Task sent but not yet processed</li>
|
||||||
|
<li><span className="text-emerald-400">working</span>: Task being processed by the agent</li>
|
||||||
|
<li><span className="text-emerald-400">completed</span>: Task completed successfully</li>
|
||||||
|
<li><span className="text-emerald-400">input-required</span>: Agent waiting for additional user input</li>
|
||||||
|
<li><span className="text-emerald-400">failed</span>: Task failed during processing</li>
|
||||||
|
<li><span className="text-emerald-400">canceled</span>: Task was canceled</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-[#222] p-4 rounded-md border border-[#444]">
|
||||||
|
<h4 className="font-medium text-white mb-2">Possible task states:</h4>
|
||||||
|
<ul className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
|
<li className="flex items-center">
|
||||||
|
<span className="w-3 h-3 bg-blue-500 rounded-full mr-2"></span>
|
||||||
|
<span className="text-neutral-300"><strong>submitted</strong>: Task sent</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center">
|
||||||
|
<span className="w-3 h-3 bg-yellow-500 rounded-full mr-2"></span>
|
||||||
|
<span className="text-neutral-300"><strong>working</strong>: Task being processed</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center">
|
||||||
|
<span className="w-3 h-3 bg-purple-500 rounded-full mr-2"></span>
|
||||||
|
<span className="text-neutral-300"><strong>input-required</strong>: Agent waiting for additional user input</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center">
|
||||||
|
<span className="w-3 h-3 bg-green-500 rounded-full mr-2"></span>
|
||||||
|
<span className="text-neutral-300"><strong>completed</strong>: Task completed successfully</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center">
|
||||||
|
<span className="w-3 h-3 bg-neutral-500 rounded-full mr-2"></span>
|
||||||
|
<span className="text-neutral-300"><strong>canceled</strong>: Task canceled</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center">
|
||||||
|
<span className="w-3 h-3 bg-red-500 rounded-full mr-2"></span>
|
||||||
|
<span className="text-neutral-300"><strong>failed</strong>: Task processing failed</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
1611
frontend/app/documentation/page.tsx
Normal file
1611
frontend/app/documentation/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
87
frontend/app/globals.css
Normal file
87
frontend/app/globals.css
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @author: Davidson Gomes │
|
||||||
|
│ @file: /app/globals.css │
|
||||||
|
│ Developed by: Davidson Gomes │
|
||||||
|
│ Creation date: May 13, 2025 │
|
||||||
|
│ Contact: contato@evolution-api.com │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||||
|
│ Licensed under the Apache License, Version 2.0 │
|
||||||
|
│ │
|
||||||
|
│ You may not use this file except in compliance with the License. │
|
||||||
|
│ You may obtain a copy of the License at │
|
||||||
|
│ │
|
||||||
|
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||||
|
│ │
|
||||||
|
│ Unless required by applicable law or agreed to in writing, software │
|
||||||
|
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||||
|
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||||
|
│ See the License for the specific language governing permissions and │
|
||||||
|
│ limitations under the License. │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ @important │
|
||||||
|
│ For any future changes to the code in this file, it is recommended to │
|
||||||
|
│ include, together with the modification, the information of the developer │
|
||||||
|
│ who changed it and the date of modification. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 240 10% 3.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
--primary: 142.1 70.6% 45.3%;
|
||||||
|
--primary-foreground: 355.7 100% 97.3%;
|
||||||
|
--secondary: 240 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
--muted: 240 4.8% 95.9%;
|
||||||
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
--accent: 240 4.8% 95.9%;
|
||||||
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
|
--input: 240 5.9% 90%;
|
||||||
|
--ring: 142.1 70.6% 45.3%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 240 10% 3.9%;
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
--card: 240 10% 3.9%;
|
||||||
|
--card-foreground: 0 0% 98%;
|
||||||
|
--popover: 240 10% 3.9%;
|
||||||
|
--popover-foreground: 0 0% 98%;
|
||||||
|
--primary: 142.1 70.6% 45.3%;
|
||||||
|
--primary-foreground: 144.9 80.4% 10%;
|
||||||
|
--secondary: 240 3.7% 15.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
--muted: 240 3.7% 15.9%;
|
||||||
|
--muted-foreground: 240 5% 64.9%;
|
||||||
|
--accent: 240 3.7% 15.9%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 3.7% 15.9%;
|
||||||
|
--input: 240 3.7% 15.9%;
|
||||||
|
--ring: 142.4 71.8% 29.2%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user