Compare commits
52 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 |
@@ -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/
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
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 }}
|
||||||
48
.github/workflows/docker-image.yml
vendored
48
.github/workflows/docker-image.yml
vendored
@@ -1,48 +0,0 @@
|
|||||||
name: Build Docker image
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "*.*.*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build_deploy:
|
|
||||||
name: Build and Deploy
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
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=semver,pattern=v{{version}}
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
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:
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
|
|
||||||
- name: Image digest
|
|
||||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
name: Build Docker image
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build_deploy:
|
|
||||||
name: Build and Deploy
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
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: homolog
|
|
||||||
|
|
||||||
- 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:
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
|
|
||||||
- name: Image digest
|
|
||||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
name: Build Docker image
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build_deploy:
|
|
||||||
name: Build and Deploy
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
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: latest
|
|
||||||
|
|
||||||
- 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:
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
|
|
||||||
- name: Image digest
|
|
||||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
|
||||||
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/
|
||||||
|
|||||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -5,6 +5,24 @@ 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.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
|
## [0.0.10] - 2025-05-15
|
||||||
|
|
||||||
### 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!
|
||||||
@@ -2,20 +2,19 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
api:
|
api:
|
||||||
image: evo-ai-api:latest
|
# image: evoapicloud/evo-ai:latest Use this image to pull from the repo
|
||||||
build: .
|
image: evoai-api:latest # Use this image for local builds
|
||||||
container_name: evo-ai-api
|
|
||||||
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:"
|
||||||
@@ -29,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
|
||||||
@@ -41,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}
|
||||||
@@ -55,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:
|
||||||
@@ -72,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:
|
||||||
@@ -83,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:
|
||||||
@@ -97,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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user