Compare commits
131 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f1d2745fd | ||
|
|
b918ffdf76 | ||
|
|
3ba85d9c37 | ||
|
|
e198d858b9 | ||
|
|
c2df6a0a5c | ||
|
|
509e03d46d | ||
|
|
d09a9ca046 | ||
|
|
17524e5dff | ||
|
|
840e55438a | ||
|
|
0b722c9852 | ||
|
|
7ff7a1455c | ||
|
|
b4939b0fca | ||
|
|
eb7bb06ef3 | ||
|
|
53e2c7016c | ||
|
|
27a367972b | ||
|
|
c9087b1918 | ||
|
|
772263f7d7 | ||
|
|
e2d3483de2 | ||
|
|
62a47cc7d2 | ||
|
|
fe778b3eb9 | ||
|
|
4e1f663787 | ||
|
|
9ef4835344 | ||
|
|
dc01331696 | ||
|
|
f1f2ba8823 | ||
|
|
027c096377 | ||
|
|
c4a4e5fd68 | ||
|
|
956d16a854 | ||
|
|
482c1693d1 | ||
|
|
e2e756156f | ||
|
|
cf24a7ce5d | ||
|
|
3e8c322e79 | ||
|
|
9135aa59d6 | ||
|
|
ef4e4ee1c7 | ||
|
|
7a9d3e1477 | ||
|
|
b619d88d4e | ||
|
|
2c7e5d0528 | ||
|
|
c469bf1998 | ||
|
|
25db7e8a9a | ||
|
|
d01644c00c | ||
|
|
86258efcbd | ||
|
|
24d7950d13 | ||
|
|
f000a08701 | ||
|
|
257d50a584 | ||
|
|
9f176bf0e0 | ||
|
|
21e67e43a3 | ||
|
|
c916b7a660 | ||
|
|
c6916eabc5 | ||
|
|
7f35a9a6bc | ||
|
|
6d7b1194d0 | ||
|
|
1bcd76595c | ||
|
|
6234e3838f | ||
|
|
ddb7650f59 | ||
|
|
43b2c03c91 | ||
|
|
eebe995826 | ||
|
|
5e854b9d1d | ||
|
|
0c96e42f51 | ||
|
|
2d98cb715f | ||
|
|
16df8a9a1b | ||
|
|
ecdd0cc917 | ||
|
|
f4878452f6 | ||
|
|
8654988f57 | ||
|
|
09eea12250 | ||
|
|
07d4fb18f3 | ||
|
|
0b12acd4ac | ||
|
|
a33881759c | ||
|
|
139497e1fa | ||
|
|
2bbe2c90ac | ||
|
|
e200ce6490 | ||
|
|
13b68095e6 | ||
|
|
7940876e6f | ||
|
|
17d72238c7 | ||
|
|
22a771abd8 | ||
|
|
1656fda8da | ||
|
|
6bf0ea52e0 | ||
|
|
add128f4d5 | ||
|
|
958eeec4a6 | ||
|
|
18c6865926 | ||
|
|
3622260c11 | ||
|
|
0ca6b4f3e9 | ||
|
|
2a80bdf7a3 | ||
|
|
198eb57032 | ||
|
|
9ab001c35e | ||
|
|
0dbf6d1c13 | ||
|
|
6cfff4cc95 | ||
|
|
98c559e1ce | ||
|
|
6a9f329def | ||
|
|
b29d8d108e | ||
|
|
72e4b7865a | ||
|
|
ef5e84859d | ||
|
|
ae62a557d3 | ||
|
|
86de80a998 | ||
|
|
2bac2b3824 | ||
|
|
3185233233 | ||
|
|
48597bdf30 | ||
|
|
b53746dd1f | ||
|
|
ebbbf62df5 | ||
|
|
d0f40e7d35 | ||
|
|
15a4ec7e33 | ||
|
|
6c0dfae9bf | ||
|
|
48d15a6128 | ||
|
|
7ddbce89d0 | ||
|
|
cf07f732c2 | ||
|
|
897d3dc9ea | ||
|
|
8438e75dff | ||
|
|
a099575620 | ||
|
|
37c17c9e3d | ||
|
|
7e013787ed | ||
|
|
984b3b28ba | ||
|
|
80f04ada32 | ||
|
|
3dd6971fce | ||
|
|
02cbda22dd | ||
|
|
e3e40ede2b | ||
|
|
0c3d2fdbe2 | ||
|
|
6be09de87d | ||
|
|
1e00887167 | ||
|
|
705791d17f | ||
|
|
0ec9bbdc13 | ||
|
|
146c28ae27 | ||
|
|
fc61fb062e | ||
|
|
a46402fd08 | ||
|
|
47307a1045 | ||
|
|
b21e355ce1 | ||
|
|
0c69df107e | ||
|
|
4800807783 | ||
|
|
71ecc8f35b | ||
|
|
782c2aceff | ||
|
|
ff27fb157c | ||
|
|
bafbd494ed | ||
|
|
ab1f528a34 | ||
|
|
f319b89806 | ||
|
|
a1f6b828d5 |
@@ -40,6 +40,12 @@ tests/
|
||||
.flake8
|
||||
requirements-dev.txt
|
||||
|
||||
# Python specific - don't exclude frontend
|
||||
!frontend/
|
||||
frontend/node_modules/
|
||||
frontend/.next/
|
||||
frontend/dist/
|
||||
|
||||
# Ambiente virtual
|
||||
venv/
|
||||
__pycache__/
|
||||
@@ -54,7 +60,7 @@ dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
# lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
|
||||
20
.env.example
20
.env.example
@@ -6,6 +6,9 @@ API_URL="http://localhost:8000"
|
||||
ORGANIZATION_NAME="Evo AI"
|
||||
ORGANIZATION_URL="https://evoai.evoapicloud.com"
|
||||
|
||||
# AI Engine configuration: "adk" or "crewai"
|
||||
AI_ENGINE="adk"
|
||||
|
||||
# Database settings
|
||||
POSTGRES_CONNECTION_STRING="postgresql://postgres:root@localhost:5432/evo_ai"
|
||||
|
||||
@@ -34,11 +37,28 @@ JWT_EXPIRATION_TIME=3600
|
||||
# Encryption key for API keys
|
||||
ENCRYPTION_KEY="your-encryption-key"
|
||||
|
||||
# Email provider settings
|
||||
EMAIL_PROVIDER="sendgrid"
|
||||
|
||||
# SendGrid
|
||||
SENDGRID_API_KEY="your-sendgrid-api-key"
|
||||
EMAIL_FROM="noreply@yourdomain.com"
|
||||
|
||||
# SMTP settings
|
||||
SMTP_HOST="your-smtp-host"
|
||||
SMTP_FROM="noreply-smtp@yourdomain.com"
|
||||
SMTP_USER="your-smtp-username"
|
||||
SMTP_PASSWORD="your-smtp-password"
|
||||
SMTP_PORT=587
|
||||
SMTP_USE_TLS=true
|
||||
SMTP_USE_SSL=false
|
||||
|
||||
APP_URL="https://yourdomain.com"
|
||||
|
||||
LANGFUSE_PUBLIC_KEY="your-langfuse-public-key"
|
||||
LANGFUSE_SECRET_KEY="your-langfuse-secret-key"
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT="https://cloud.langfuse.com/api/public/otel"
|
||||
|
||||
# Server settings
|
||||
HOST="0.0.0.0"
|
||||
PORT=8000
|
||||
|
||||
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 }}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,7 +11,7 @@ dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
# lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
|
||||
101
CHANGELOG.md
Normal file
101
CHANGELOG.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# 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.1.0] - 2025-05-24
|
||||
|
||||
### Added
|
||||
|
||||
- Export and Import Agents
|
||||
|
||||
### Changed
|
||||
|
||||
- A2A implementation updated to version 0.2.1 (https://google.github.io/A2A/specification/#agent2agent-a2a-protocol-specification)
|
||||
- Frontend redesign
|
||||
- Fixed message order
|
||||
|
||||
## [0.0.11] - 2025-05-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixes in email service and client service
|
||||
|
||||
## [0.0.10] - 2025-05-15
|
||||
|
||||
### Added
|
||||
|
||||
- Add Task Agent for structured single-task execution
|
||||
- Improve context management in agent execution
|
||||
- Add file support for A2A protocol (Agent-to-Agent) endpoints
|
||||
- Implement multimodal content processing in A2A messages
|
||||
- Add SMTP email provider support as alternative to SendGrid
|
||||
|
||||
## [0.0.9] - 2025-05-13
|
||||
|
||||
### Added
|
||||
|
||||
- Add API key sharing and flexible authentication for chat routes
|
||||
|
||||
### Changed
|
||||
|
||||
- Enhance user authentication with detailed error handling
|
||||
|
||||
## [0.0.8] - 2025-05-13
|
||||
|
||||
### Changed
|
||||
|
||||
- Update author information in multiple files
|
||||
|
||||
## [0.0.7] - 2025-05-13
|
||||
|
||||
### Added
|
||||
|
||||
- Docker image CI workflow for automated builds and pushes
|
||||
- GitHub Container Registry (GHCR) integration
|
||||
- Automated image tagging based on branch and commit
|
||||
- Docker Buildx setup for multi-platform builds
|
||||
- Cache optimization for faster builds
|
||||
- Automated image publishing on push to main and develop branches
|
||||
|
||||
## [0.0.6] - 2025-05-13
|
||||
|
||||
### Added
|
||||
|
||||
- Initial public release of Evo AI platform
|
||||
- FastAPI-based backend API
|
||||
- JWT authentication with email verification
|
||||
- Agent management (LLM, A2A, Sequential, Parallel, Loop, Workflow)
|
||||
- Agent 2 Agent (A2A) protocol support (Google A2A spec)
|
||||
- MCP server integration and management
|
||||
- Custom tools management for agents
|
||||
- Folder-based agent organization
|
||||
- Secure API key management with encryption
|
||||
- PostgreSQL and Redis integration
|
||||
- Email notifications (SendGrid) with Jinja2 templates
|
||||
- Audit log system for administrative actions
|
||||
- LangGraph integration for workflow agents
|
||||
- OpenTelemetry tracing and Langfuse integration
|
||||
- Docker and Docker Compose support
|
||||
- English documentation and codebase
|
||||
|
||||
### Changed
|
||||
|
||||
- N/A
|
||||
|
||||
### Fixed
|
||||
|
||||
- N/A
|
||||
|
||||
### Security
|
||||
|
||||
- JWT tokens with expiration and resource-based access control
|
||||
- Secure password hashing (bcrypt)
|
||||
- Account lockout after multiple failed login attempts
|
||||
- Email verification and password reset flows
|
||||
|
||||
---
|
||||
|
||||
Older versions and future releases will be listed here.
|
||||
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 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
|
||||
201
LICENSE
Normal file
201
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.
|
||||
2
Makefile
2
Makefile
@@ -18,7 +18,7 @@ alembic-downgrade:
|
||||
|
||||
# Command to run the server
|
||||
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
|
||||
run-prod:
|
||||
|
||||
75
SECURITY.md
Normal file
75
SECURITY.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We aim to support the latest stable release of Evo AI and apply security updates as soon as possible. Please use the most recent version for the best security.
|
||||
|
||||
---
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in Evo AI, **please report it privately** and responsibly. Do **not** open a public issue.
|
||||
|
||||
**To report a vulnerability:**
|
||||
|
||||
- Email: [contato@evolution-api.com](mailto:contato@evolution-api.com)
|
||||
- Include as much detail as possible, including:
|
||||
- Steps to reproduce the issue
|
||||
- Potential impact
|
||||
- Your suggestions (if any) for remediation
|
||||
|
||||
You will receive a response as soon as possible. We may request additional information to fully understand and address the issue.
|
||||
|
||||
---
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
- **Keep your installation up to date.**
|
||||
Always use the latest stable version and regularly check for updates.
|
||||
|
||||
- **Environment Variables:**
|
||||
Store all secrets, credentials, and keys in environment variables or secrets managers.
|
||||
Never commit sensitive information to the repository.
|
||||
|
||||
- **Authentication:**
|
||||
Evo AI uses JWT authentication with expiration, email verification, and account lockout for brute-force protection.
|
||||
|
||||
- **Passwords:**
|
||||
All passwords are securely hashed with bcrypt and random salt.
|
||||
|
||||
- **Access Control:**
|
||||
Access to sensitive endpoints is protected via role-based checks and resource ownership verification.
|
||||
|
||||
- **Audit Logs:**
|
||||
Important administrative actions are logged for traceability.
|
||||
|
||||
- **Input Validation:**
|
||||
All inputs are validated using Pydantic schemas to prevent injection attacks.
|
||||
|
||||
---
|
||||
|
||||
## Responsible Disclosure
|
||||
|
||||
Please give us a reasonable time to investigate and address any reported security issues before any public disclosure.
|
||||
|
||||
---
|
||||
|
||||
## Project Security Features
|
||||
|
||||
- JWT tokens with limited lifetime
|
||||
- Secure password hashing (bcrypt)
|
||||
- Email verification with one-time tokens
|
||||
- Account lockout after multiple failed login attempts
|
||||
- Resource-based access control
|
||||
- Strict input validation for all APIs
|
||||
- Separation between regular and administrative users
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
All security contributions are made under the [Apache License 2.0](./LICENSE).
|
||||
|
||||
---
|
||||
|
||||
Thank you for helping keep Evo AI and its users safe!
|
||||
41
conftest.py
41
conftest.py
@@ -1,3 +1,32 @@
|
||||
"""
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: conftest.py │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
@@ -22,11 +51,11 @@ TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engin
|
||||
def db_session():
|
||||
"""Creates a fresh database session for each test."""
|
||||
Base.metadata.create_all(bind=engine) # Create tables
|
||||
|
||||
|
||||
connection = engine.connect()
|
||||
transaction = connection.begin()
|
||||
session = TestingSessionLocal(bind=connection)
|
||||
|
||||
|
||||
# Use our test database instead of the standard one
|
||||
def override_get_db():
|
||||
try:
|
||||
@@ -34,11 +63,11 @@ def db_session():
|
||||
session.commit()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
|
||||
yield session # The test will run here
|
||||
|
||||
|
||||
# Teardown
|
||||
transaction.rollback()
|
||||
connection.close()
|
||||
@@ -50,4 +79,4 @@ def db_session():
|
||||
def client(db_session):
|
||||
"""Creates a FastAPI TestClient with database session fixture."""
|
||||
with TestClient(app) as test_client:
|
||||
yield test_client
|
||||
yield test_client
|
||||
|
||||
@@ -2,19 +2,19 @@ version: "3.8"
|
||||
|
||||
services:
|
||||
api:
|
||||
build: .
|
||||
container_name: evo-ai-api
|
||||
# image: evoapicloud/evo-ai:latest Use this image to pull from the repo
|
||||
image: evoai-api:latest # Use this image for local builds
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
- postgres
|
||||
- redis
|
||||
ports:
|
||||
- "8000:8000"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
POSTGRES_CONNECTION_STRING: postgresql://postgres:${POSTGRES_PASSWORD:-postgres}@postgres:5432/evo_ai
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
REDIS_PORT: ${REDIS_PORT:-6379}
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD:-""}
|
||||
REDIS_SSL: "false"
|
||||
REDIS_KEY_PREFIX: "a2a:"
|
||||
@@ -28,7 +28,6 @@ services:
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- ./static:/app/static
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/"]
|
||||
interval: 30s
|
||||
@@ -40,12 +39,9 @@ services:
|
||||
limits:
|
||||
cpus: "1"
|
||||
memory: 1G
|
||||
networks:
|
||||
- evo-network
|
||||
|
||||
postgres:
|
||||
image: postgres:14-alpine
|
||||
container_name: evo-ai-postgres
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||
@@ -54,15 +50,12 @@ services:
|
||||
- "${POSTGRES_PORT:-5432}:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
networks:
|
||||
- evo-network
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
@@ -71,8 +64,12 @@ services:
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
container_name: evo-ai-redis
|
||||
command: redis-server --appendonly yes ${REDIS_PASSWORD:+--requirepass ${REDIS_PASSWORD}}
|
||||
command:
|
||||
- redis-server
|
||||
- --appendonly
|
||||
- "yes"
|
||||
- --requirepass
|
||||
- "${REDIS_PASSWORD}"
|
||||
ports:
|
||||
- "${REDIS_PORT:-6379}:6379"
|
||||
volumes:
|
||||
@@ -82,9 +79,6 @@ services:
|
||||
interval: 5s
|
||||
timeout: 30s
|
||||
retries: 50
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- evo-network
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
@@ -96,8 +90,3 @@ volumes:
|
||||
name: ${POSTGRES_VOLUME_NAME:-evo-ai-postgres-data}
|
||||
redis_data:
|
||||
name: ${REDIS_VOLUME_NAME:-evo-ai-redis-data}
|
||||
|
||||
networks:
|
||||
evo-network:
|
||||
name: ${NETWORK_NAME:-evo-network}
|
||||
driver: bridge
|
||||
|
||||
120
frontend/.cursorrules
Normal file
120
frontend/.cursorrules
Normal file
@@ -0,0 +1,120 @@
|
||||
# Next.js Project Rules
|
||||
|
||||
## Language
|
||||
- All code, comments, documentation, commits, and PRs MUST be written in English.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Folder Structure
|
||||
- `/app`: App router pages and API routes
|
||||
- Route-specific components should be placed in their respective route folders
|
||||
- `/components`: Reusable UI components
|
||||
- `/ui`: Shadcn UI components and their derivatives
|
||||
- `/contexts`: React Context providers
|
||||
- `/hooks`: Custom React hooks
|
||||
- `/lib`: Utility functions and configuration
|
||||
- `/public`: Static assets
|
||||
- `/services`: API service functions
|
||||
- `/styles`: Global styles
|
||||
- `/types`: TypeScript type definitions
|
||||
|
||||
### Component Guidelines
|
||||
- Use functional components with TypeScript
|
||||
- Use the `.tsx` extension for React components
|
||||
- Follow a logical naming convention:
|
||||
- Complex components: Use PascalCase and create folders with an index.tsx file
|
||||
- Simple components: Single PascalCase named files
|
||||
|
||||
### State Management
|
||||
- Use React Context for global state
|
||||
- Use React hooks for local state
|
||||
- Avoid prop drilling more than 2 levels deep
|
||||
|
||||
### API & Data Fetching
|
||||
- Use API service modules in `/services` directory
|
||||
- Implement proper error handling and loading states
|
||||
- Use React Query or SWR for complex data fetching where appropriate
|
||||
|
||||
## Development Patterns
|
||||
|
||||
### Code Quality
|
||||
- Maintain type safety - avoid using `any` type
|
||||
- Write self-documenting code with descriptive names
|
||||
- Keep components focused on a single responsibility
|
||||
- Extract complex logic into custom hooks
|
||||
- Follow DRY (Don't Repeat Yourself) principle
|
||||
|
||||
### CSS & Styling
|
||||
- Use Tailwind CSS for styling
|
||||
- Use Shadcn UI components as base building blocks
|
||||
- Maintain consistent spacing and sizing
|
||||
|
||||
### Performance
|
||||
- Avoid unnecessary re-renders
|
||||
- Optimize images and assets
|
||||
- Implement code splitting where appropriate
|
||||
- Use dynamic imports for large components/pages
|
||||
|
||||
### Testing
|
||||
- Write tests for critical business logic
|
||||
- Test components in isolation
|
||||
- Implement end-to-end tests for critical user flows
|
||||
|
||||
## Git Workflow
|
||||
|
||||
### Branch Naming
|
||||
- Features: `feature/short-description`
|
||||
- Bugfixes: `fix/short-description`
|
||||
- Hotfixes: `hotfix/short-description`
|
||||
- Releases: `release/version`
|
||||
|
||||
## Conventions
|
||||
- Variable and function names in English
|
||||
- Log and error messages in English
|
||||
- Documentation in English
|
||||
- User-facing content (emails, responses) in English
|
||||
- Indentation with 4 spaces
|
||||
- Maximum of 79 characters per line
|
||||
|
||||
## Commit Rules
|
||||
- Use Conventional Commits format for all commit messages
|
||||
- Format: `<type>(<scope>): <description>`
|
||||
- Types:
|
||||
- `feat`: A new feature
|
||||
- `fix`: A bug fix
|
||||
- `docs`: Documentation changes
|
||||
- `style`: Changes that do not affect code meaning (formatting, etc.)
|
||||
- `refactor`: Code changes that neither fix a bug nor add a feature
|
||||
- `perf`: Performance improvements
|
||||
- `test`: Adding or modifying tests
|
||||
- `chore`: Changes to build process or auxiliary tools
|
||||
- Scope is optional and should be the module or component affected
|
||||
- Description should be concise, in the imperative mood, and not capitalized
|
||||
- Use body for more detailed explanations if needed
|
||||
- Reference issues in the footer with `Fixes #123` or `Relates to #123`
|
||||
- Examples:
|
||||
- `feat(auth): add password reset functionality`
|
||||
- `fix(api): correct validation error in client registration`
|
||||
- `docs: update API documentation for new endpoints`
|
||||
- `refactor(services): improve error handling in authentication`
|
||||
|
||||
Format: `type(scope): subject`
|
||||
|
||||
Examples:
|
||||
- `feat(auth): add login form validation`
|
||||
- `fix(api): resolve user data fetching issue`
|
||||
- `docs(readme): update installation instructions`
|
||||
- `style(components): format according to style guide`
|
||||
|
||||
### Pull Requests
|
||||
- Keep PRs focused on a single feature or fix
|
||||
- Include descriptive titles and descriptions
|
||||
- Reference related issues
|
||||
- Request code reviews from appropriate team members
|
||||
- Ensure CI checks pass before merging
|
||||
|
||||
## Code Review Guidelines
|
||||
- Focus on code quality, architecture, and maintainability
|
||||
- Provide constructive feedback
|
||||
- Address all review comments before merging
|
||||
- Maintain a respectful and collaborative tone
|
||||
61
frontend/.dockerignore
Normal file
61
frontend/.dockerignore
Normal file
@@ -0,0 +1,61 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Production builds
|
||||
.next/
|
||||
out/
|
||||
dist/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Temporary files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# IDEs and editors
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Docker
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
.nyc_output
|
||||
.coverage
|
||||
|
||||
# Other
|
||||
.cache/
|
||||
1
frontend/.env.example
Normal file
1
frontend/.env.example
Normal file
@@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||
31
frontend/.gitignore
vendored
Normal file
31
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Lock files
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# env files
|
||||
.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
67
frontend/CHANGELOG.md
Normal file
67
frontend/CHANGELOG.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.0.7] - 2025-05-15
|
||||
|
||||
### Added
|
||||
|
||||
- Add Task agents
|
||||
- Add file support for A2A protocol (Agent-to-Agent) endpoints
|
||||
- Add entrypoint script for dynamic environment variable handling
|
||||
- Add agent card URL input and copy functionality
|
||||
|
||||
## [0.0.6] - 2025-05-13
|
||||
|
||||
### Added
|
||||
|
||||
- Agent sharing functionality with third parties via API keys
|
||||
- Dedicated shared-chat page for accessing shared agents
|
||||
- Local storage mechanism to save recently used shared agents
|
||||
- Public access to shared agents without full authentication
|
||||
|
||||
### Changed
|
||||
|
||||
- Add example environment file and update .gitignore
|
||||
- Add clientId prop to agent-related components and improve agent data processing
|
||||
- Refactor middleware to handle shared agent routes as public paths
|
||||
- Update API interceptors to prevent forced logout on shared chat pages
|
||||
|
||||
### security
|
||||
|
||||
- Implement force logout functionality on 401 Unauthorized responses
|
||||
|
||||
## [0.0.5] - 2025-05-13
|
||||
|
||||
### Changed
|
||||
|
||||
- Update author information in multiple files
|
||||
|
||||
## [0.0.4] - 2025-05-13
|
||||
|
||||
### Added
|
||||
- Initial public release
|
||||
- User-friendly interface for creating and managing AI agents
|
||||
- Integration with multiple language models (e.g., GPT-4, Claude)
|
||||
- Client management interface
|
||||
- Visual configuration for MCP servers
|
||||
- Custom tools management
|
||||
- JWT authentication with email verification
|
||||
- Agent 2 Agent (A2A) protocol support (Google's A2A spec)
|
||||
- Workflow Agent with ReactFlow for visual workflow creation
|
||||
- Secure API key management (encrypted storage)
|
||||
- Agent organization with folders and categories
|
||||
- Dashboard with agent overview, usage stats, and recent activities
|
||||
- Agent editor for creating, editing, and configuring agents
|
||||
- Workflow editor for building and visualizing agent flows
|
||||
- API key manager for adding, encrypting, and rotating keys
|
||||
- RESTful API and WebSocket backend integration
|
||||
- Docker support for containerized deployment
|
||||
- Complete documentation and contribution guidelines
|
||||
|
||||
---
|
||||
|
||||
Older versions and future releases will be listed here.
|
||||
60
frontend/Dockerfile
Normal file
60
frontend/Dockerfile
Normal file
@@ -0,0 +1,60 @@
|
||||
# Build stage
|
||||
FROM node:20.15.1-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Define build arguments with default values
|
||||
ARG NEXT_PUBLIC_API_URL=https://api.evo-ai.co
|
||||
|
||||
# Install pnpm globally
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Copy package files first for better caching
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --no-frozen-lockfile
|
||||
|
||||
# Copy all source code (this includes tsconfig.json, lib/, etc.)
|
||||
COPY . .
|
||||
|
||||
# Set environment variables from build arguments
|
||||
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||
|
||||
# Build the application
|
||||
RUN pnpm run build
|
||||
|
||||
# Production stage
|
||||
FROM node:20.15.1-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Define build arguments again for the runner stage
|
||||
ARG NEXT_PUBLIC_API_URL=https://api-evoai.evoapicloud.com
|
||||
|
||||
# Install pnpm globally
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Install production dependencies only
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install --prod --no-frozen-lockfile
|
||||
|
||||
# Copy built assets from builder
|
||||
COPY --from=builder /app/.next ./.next
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/next.config.mjs ./
|
||||
|
||||
# Set environment variables
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||
|
||||
# Script to replace environment variables at runtime
|
||||
COPY docker-entrypoint.sh ./
|
||||
RUN chmod +x ./docker-entrypoint.sh
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Use entrypoint script to initialize environment variables before starting the app
|
||||
ENTRYPOINT ["sh", "./docker-entrypoint.sh"]
|
||||
201
frontend/LICENSE
Normal file
201
frontend/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2025 Evolution API
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
237
frontend/README.md
Normal file
237
frontend/README.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# Evo AI - AI Agents Platform (Frontend)
|
||||
|
||||
Evo AI is an open-source platform for creating and managing AI agents, enabling integration with different AI models and services.
|
||||
|
||||
## 🚀 Overview
|
||||
|
||||
The Evo AI frontend platform enables:
|
||||
|
||||
- User-friendly interface for creating and managing AI agents
|
||||
- Integration with different language models
|
||||
- Client management
|
||||
- Visual configuration of MCP servers
|
||||
- Custom tools management
|
||||
- JWT authentication with email verification
|
||||
- **Agent 2 Agent (A2A) Protocol Support**: Interface for interoperability between AI agents following Google's A2A specification
|
||||
- **Workflow Agent with ReactFlow**: Visual interface for building complex agent workflows
|
||||
- **Secure API Key Management**: Interface for encrypted storage of API keys
|
||||
- **Agent Organization**: Folder structure for organizing agents by categories
|
||||
|
||||
## 🧩 Agent Creation Interface
|
||||
|
||||
The frontend offers intuitive interfaces for creating different types of agents:
|
||||
|
||||
### 1. LLM Agent (Language Model)
|
||||
|
||||
Interface for configuring agents based on models like GPT-4, Claude, etc. with tools, MCP servers, and sub-agents.
|
||||
|
||||
### 2. A2A Agent (Agent-to-Agent)
|
||||
|
||||
Interface for implementing Google's A2A protocol for agent interoperability.
|
||||
|
||||
### 3. Sequential Agent
|
||||
|
||||
Interface for executing sub-agents in a specific order.
|
||||
|
||||
### 4. Parallel Agent
|
||||
|
||||
Interface for executing multiple sub-agents simultaneously.
|
||||
|
||||
### 5. Loop Agent
|
||||
|
||||
Interface for executing sub-agents in a loop with a defined number of iterations.
|
||||
|
||||
### 6. Workflow Agent
|
||||
|
||||
Visual interface based on ReactFlow for creating complex workflows between agents.
|
||||
|
||||
## 🛠️ Technologies
|
||||
|
||||
- [Next.js](https://nextjs.org/) - React framework for production
|
||||
- [React](https://reactjs.org/) - JavaScript library for building user interfaces
|
||||
- [Tailwind CSS](https://tailwindcss.com/) - Utility-first CSS framework
|
||||
- [Shadcn UI](https://ui.shadcn.com/) - UI component library
|
||||
- [Radix UI](https://www.radix-ui.com/) - Unstyled, accessible components
|
||||
- [TypeScript](https://www.typescriptlang.org/) - Typed JavaScript
|
||||
- [React Query](https://tanstack.com/query/latest) - Data fetching and state management
|
||||
- [Zustand](https://zustand-demo.pmnd.rs/) - Global state management
|
||||
- [React Flow](https://reactflow.dev/) - Library for building node-based visual workflows
|
||||
- [Axios](https://axios-http.com/) - HTTP client for API communication
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- Node.js 18+ (LTS recommended)
|
||||
- npm, yarn, or pnpm package manager
|
||||
- Evo AI backend running
|
||||
|
||||
## 🔧 Installation
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/EvolutionAPI/evo-ai-frontend.git
|
||||
cd evo-ai-frontend
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
# or
|
||||
yarn install
|
||||
# or
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. Configure environment variables:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit the .env file with your settings
|
||||
```
|
||||
|
||||
## 🚀 Running the Project
|
||||
|
||||
```bash
|
||||
# Development mode
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
|
||||
# Production build
|
||||
npm run build
|
||||
# or
|
||||
yarn build
|
||||
# or
|
||||
pnpm build
|
||||
|
||||
# Start production server
|
||||
npm run start
|
||||
# or
|
||||
yarn start
|
||||
# or
|
||||
pnpm start
|
||||
```
|
||||
|
||||
The project will be available at [http://localhost:3000](http://localhost:3000)
|
||||
|
||||
## 🔐 Authentication
|
||||
|
||||
The frontend implements JWT authentication integrated with the backend:
|
||||
|
||||
- **User Registration**: Form for creating new accounts
|
||||
- **Email Verification**: Process for verifying via email
|
||||
- **Login**: Authentication of existing users
|
||||
- **Password Recovery**: Complete password recovery flow
|
||||
- **Secure Storage**: Tokens stored in HttpOnly cookies
|
||||
|
||||
## 🖥️ Main Interface Features
|
||||
|
||||
### Dashboard
|
||||
|
||||
Main dashboard showing:
|
||||
- Agent overview
|
||||
- Usage statistics
|
||||
- Recent activities
|
||||
- Quick links for agent creation
|
||||
|
||||
### Agent Editor
|
||||
|
||||
Complete interface for:
|
||||
- Creating new agents
|
||||
- Editing existing agents
|
||||
- Configuring instructions
|
||||
- Selecting models
|
||||
- Setting up API keys
|
||||
|
||||
### Workflow Editor
|
||||
|
||||
Visual editor based on ReactFlow for:
|
||||
- Creating complex workflows
|
||||
- Connecting different agents
|
||||
- Defining conditionals and decision flows
|
||||
- Visualizing data flow
|
||||
|
||||
### API Key Manager
|
||||
|
||||
Interface for:
|
||||
- Adding new API keys
|
||||
- Securely encrypting keys
|
||||
- Managing existing keys
|
||||
- Rotating and updating keys
|
||||
|
||||
### Agent Organization
|
||||
|
||||
System for:
|
||||
- Creating folders and categories
|
||||
- Organizing agents by type or use case
|
||||
- Searching and filtering agents
|
||||
|
||||
## 🔄 Backend Integration
|
||||
|
||||
The frontend communicates with the backend through:
|
||||
|
||||
- **RESTful API**: Endpoints for resource management
|
||||
- **WebSockets**: Real-time communication for agent messages
|
||||
- **Response Streaming**: Support for streaming model responses
|
||||
|
||||
## 🐳 Docker Support
|
||||
|
||||
The project includes Docker configuration for containerized deployment:
|
||||
|
||||
```bash
|
||||
# Build the Docker image
|
||||
./docker_build.sh
|
||||
# or
|
||||
docker build -t nextjs-frontend .
|
||||
|
||||
# Run the container
|
||||
docker run -p 3000:3000 nextjs-frontend
|
||||
```
|
||||
|
||||
# 🐳 Docker Compose
|
||||
```bash
|
||||
# Copy the .env file
|
||||
cp .env.example .env
|
||||
|
||||
# Build and deploy
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions from the community! Here's how you can help:
|
||||
|
||||
1. Fork the project
|
||||
2. Create a feature branch (`git checkout -b feature/AmazingFeature`)
|
||||
3. Make your changes and add tests if possible
|
||||
4. Run tests and make sure they pass
|
||||
5. Commit your changes following conventional commits format (`feat: add amazing feature`)
|
||||
6. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||
7. Open a Pull Request
|
||||
|
||||
Please read our [Contributing Guidelines](CONTRIBUTING.md) for more details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
**Trademark Notice:** The name "Evo AI" and related branding are protected trademarks. Unauthorized use is prohibited.
|
||||
|
||||
## 👨💻 Development Commands
|
||||
|
||||
- `npm run dev` - Start the development server
|
||||
- `npm run build` - Build the application for production
|
||||
- `npm run start` - Start the production server
|
||||
- `npm run lint` - Run ESLint to check code quality
|
||||
- `npm run format` - Format code with Prettier
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- [Next.js](https://nextjs.org/)
|
||||
- [React](https://reactjs.org/)
|
||||
- [Tailwind CSS](https://tailwindcss.com/)
|
||||
- [Shadcn UI](https://ui.shadcn.com/)
|
||||
- [ReactFlow](https://reactflow.dev/)
|
||||
506
frontend/app/agents/AgentCard.tsx
Normal file
506
frontend/app/agents/AgentCard.tsx
Normal file
@@ -0,0 +1,506 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/AgentCard.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Folder } from "@/services/agentService";
|
||||
import { Agent, AgentType } from "@/types/agent";
|
||||
import { MCPServer } from "@/types/mcpServer";
|
||||
import {
|
||||
ArrowRight,
|
||||
Bot,
|
||||
BookOpenCheck,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Code,
|
||||
ExternalLink,
|
||||
GitBranch,
|
||||
MoveRight,
|
||||
Pencil,
|
||||
RefreshCw,
|
||||
Settings,
|
||||
Share2,
|
||||
Trash2,
|
||||
Workflow,
|
||||
TextSelect,
|
||||
Download,
|
||||
FlaskConical,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { exportAsJson } from "@/lib/utils";
|
||||
|
||||
interface AgentCardProps {
|
||||
agent: Agent;
|
||||
onEdit: (agent: Agent) => void;
|
||||
onDelete: (agent: Agent) => void;
|
||||
onMove: (agent: Agent) => void;
|
||||
onShare?: (agent: Agent) => void;
|
||||
onWorkflow?: (agentId: string) => void;
|
||||
availableMCPs?: MCPServer[];
|
||||
getApiKeyNameById?: (id: string | undefined) => string | null;
|
||||
getAgentNameById?: (id: string) => string;
|
||||
folders?: Folder[];
|
||||
agents: Agent[];
|
||||
}
|
||||
|
||||
export function AgentCard({
|
||||
agent,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onMove,
|
||||
onShare,
|
||||
onWorkflow,
|
||||
availableMCPs = [],
|
||||
getApiKeyNameById = () => null,
|
||||
getAgentNameById = (id) => id,
|
||||
folders = [],
|
||||
agents,
|
||||
}: AgentCardProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const getAgentTypeInfo = (type: AgentType) => {
|
||||
const types: Record<
|
||||
string,
|
||||
{
|
||||
label: string;
|
||||
icon: React.ElementType;
|
||||
color: string;
|
||||
bgColor: string;
|
||||
badgeClass: string;
|
||||
}
|
||||
> = {
|
||||
llm: {
|
||||
label: "LLM Agent",
|
||||
icon: Code,
|
||||
color: "#00cc7d",
|
||||
bgColor: "bg-green-500/10",
|
||||
badgeClass:
|
||||
"bg-green-900/30 text-green-400 border-green-600/30 hover:bg-green-900/40",
|
||||
},
|
||||
a2a: {
|
||||
label: "A2A Agent",
|
||||
icon: ExternalLink,
|
||||
color: "#6366f1",
|
||||
bgColor: "bg-indigo-500/10",
|
||||
badgeClass:
|
||||
"bg-indigo-900/30 text-indigo-400 border-indigo-600/30 hover:bg-indigo-900/40",
|
||||
},
|
||||
sequential: {
|
||||
label: "Sequential Agent",
|
||||
icon: ArrowRight,
|
||||
color: "#f59e0b",
|
||||
bgColor: "bg-yellow-500/10",
|
||||
badgeClass:
|
||||
"bg-yellow-900/30 text-yellow-400 border-yellow-600/30 hover:bg-yellow-900/40",
|
||||
},
|
||||
parallel: {
|
||||
label: "Parallel Agent",
|
||||
icon: GitBranch,
|
||||
color: "#8b5cf6",
|
||||
bgColor: "bg-purple-500/10",
|
||||
badgeClass:
|
||||
"bg-purple-900/30 text-purple-400 border-purple-600/30 hover:bg-purple-900/40",
|
||||
},
|
||||
loop: {
|
||||
label: "Loop Agent",
|
||||
icon: RefreshCw,
|
||||
color: "#ec4899",
|
||||
bgColor: "bg-pink-500/10",
|
||||
badgeClass:
|
||||
"bg-orange-900/30 text-orange-400 border-orange-600/30 hover:bg-orange-900/40",
|
||||
},
|
||||
workflow: {
|
||||
label: "Workflow Agent",
|
||||
icon: Workflow,
|
||||
color: "#3b82f6",
|
||||
bgColor: "bg-blue-500/10",
|
||||
badgeClass:
|
||||
"bg-blue-900/30 text-blue-400 border-blue-700/40 hover:bg-blue-900/40",
|
||||
},
|
||||
task: {
|
||||
label: "Task Agent",
|
||||
icon: BookOpenCheck,
|
||||
color: "#ef4444",
|
||||
bgColor: "bg-red-500/10",
|
||||
badgeClass:
|
||||
"bg-red-900/30 text-red-400 border-red-600/30 hover:bg-red-900/40",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
types[type] || {
|
||||
label: type,
|
||||
icon: Bot,
|
||||
color: "#94a3b8",
|
||||
bgColor: "bg-slate-500/10",
|
||||
badgeClass:
|
||||
"bg-slate-900/30 text-slate-400 border-slate-600/30 hover:bg-slate-900/40",
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const getAgentTypeIcon = (type: AgentType) => {
|
||||
const typeInfo = getAgentTypeInfo(type);
|
||||
const IconComponent = typeInfo.icon;
|
||||
return (
|
||||
<IconComponent className="h-5 w-5" style={{ color: typeInfo.color }} />
|
||||
);
|
||||
};
|
||||
|
||||
const getAgentTypeName = (type: AgentType) => {
|
||||
return getAgentTypeInfo(type).label;
|
||||
};
|
||||
|
||||
const getAgentTypeBgColor = (type: AgentType) => {
|
||||
return getAgentTypeInfo(type).bgColor;
|
||||
};
|
||||
|
||||
const getAgentTypeBadgeClass = (type: AgentType) => {
|
||||
return getAgentTypeInfo(type).badgeClass;
|
||||
};
|
||||
|
||||
const getFolderNameById = (id: string) => {
|
||||
const folder = folders?.find((f) => f.id === id);
|
||||
return folder?.name || id;
|
||||
};
|
||||
|
||||
const getTotalTools = () => {
|
||||
if (agent.type === "llm" && agent.config?.mcp_servers) {
|
||||
return agent.config.mcp_servers.reduce(
|
||||
(total, mcp) => total + (mcp.tools?.length || 0),
|
||||
0
|
||||
);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const getCreatedAtFormatted = () => {
|
||||
return new Date(agent.created_at).toLocaleDateString();
|
||||
};
|
||||
|
||||
// Function to export the agent as JSON
|
||||
const handleExportAgent = () => {
|
||||
try {
|
||||
exportAsJson(
|
||||
agent,
|
||||
`agent-${agent.name
|
||||
.replace(/\s+/g, "-")
|
||||
.toLowerCase()}-${agent.id.substring(0, 8)}`,
|
||||
true,
|
||||
agents
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error exporting agent:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to test the A2A agent in the lab
|
||||
const handleTestA2A = () => {
|
||||
// Use the agent card URL as base for A2A tests
|
||||
const agentUrl = agent.agent_card_url?.replace(
|
||||
"/.well-known/agent.json",
|
||||
""
|
||||
);
|
||||
|
||||
// Use the API key directly from the agent config
|
||||
const apiKey = agent.config?.api_key;
|
||||
|
||||
// Build the URL with parameters for the lab tests
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (agentUrl) {
|
||||
params.set("agent_url", agentUrl);
|
||||
}
|
||||
|
||||
if (apiKey) {
|
||||
params.set("api_key", apiKey);
|
||||
}
|
||||
|
||||
// Redirect to the lab tests in the "lab" tab
|
||||
const testUrl = `/documentation?${params.toString()}#lab`;
|
||||
|
||||
router.push(testUrl);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full overflow-hidden border border-zinc-800 shadow-lg bg-gradient-to-br from-zinc-800 to-zinc-900">
|
||||
<div
|
||||
className={cn(
|
||||
"p-4 flex justify-between items-center border-b border-zinc-800",
|
||||
getAgentTypeBgColor(agent.type)
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{getAgentTypeIcon(agent.type)}
|
||||
<h3 className="font-medium text-white">{agent.name}</h3>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn("border", getAgentTypeBadgeClass(agent.type))}
|
||||
>
|
||||
{getAgentTypeName(agent.type)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<CardContent className="p-0">
|
||||
<div className="p-4 border-b border-zinc-800">
|
||||
<p className="text-sm text-zinc-300">
|
||||
{agent.description && agent.description.length > 100
|
||||
? `${agent.description.substring(0, 100)}...`
|
||||
: agent.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"p-4 flex justify-between items-center",
|
||||
getAgentTypeBgColor(agent.type),
|
||||
"bg-opacity-20"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-zinc-500">Model:</span>
|
||||
<span className="text-xs font-medium text-zinc-300">
|
||||
{agent.type === "llm" ? agent.model : "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn("p-0 h-auto", {
|
||||
"text-green-400 hover:text-green-300": agent.type === "llm",
|
||||
"text-indigo-400 hover:text-indigo-300": agent.type === "a2a",
|
||||
"text-yellow-400 hover:text-yellow-300":
|
||||
agent.type === "sequential",
|
||||
"text-purple-400 hover:text-purple-300":
|
||||
agent.type === "parallel",
|
||||
"text-orange-400 hover:text-orange-300": agent.type === "loop",
|
||||
"text-blue-400 hover:text-blue-300": agent.type === "workflow",
|
||||
"text-red-400 hover:text-red-300": agent.type === "task",
|
||||
"text-zinc-400 hover:text-white": ![
|
||||
"llm",
|
||||
"a2a",
|
||||
"sequential",
|
||||
"parallel",
|
||||
"loop",
|
||||
"workflow",
|
||||
"task",
|
||||
].includes(agent.type),
|
||||
})}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{expanded ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
<span className="ml-1 text-xs">{expanded ? "Less" : "More"}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className="p-4 bg-zinc-950 text-xs space-y-3 animate-in fade-in-50 duration-200">
|
||||
{agent.folder_id && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-zinc-500">Folder:</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"h-5 px-2 bg-transparent",
|
||||
getAgentTypeBadgeClass(agent.type)
|
||||
)}
|
||||
>
|
||||
{getFolderNameById(agent.folder_id)}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{agent.type === "llm" && agent.api_key_id && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-zinc-500">API Key:</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"h-5 px-2 bg-transparent",
|
||||
getAgentTypeBadgeClass(agent.type)
|
||||
)}
|
||||
>
|
||||
{getApiKeyNameById(agent.api_key_id)}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{getTotalTools() > 0 && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-zinc-500">Tools:</span>
|
||||
<span className="text-zinc-300">{getTotalTools()}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{agent.config?.sub_agents && agent.config.sub_agents.length > 0 && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-zinc-500">Sub-agents:</span>
|
||||
<span className="text-zinc-300">
|
||||
{agent.config.sub_agents.length}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{agent.type === "workflow" && agent.config?.workflow && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-zinc-500">Elements:</span>
|
||||
<span className="text-zinc-300">
|
||||
{agent.config.workflow.nodes?.length || 0} nodes,{" "}
|
||||
{agent.config.workflow.edges?.length || 0} connections
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-zinc-500">Created at:</span>
|
||||
<span className="text-zinc-300">{getCreatedAtFormatted()}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-zinc-500">ID:</span>
|
||||
<span className="text-zinc-300 text-[10px]">{agent.id}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex border-t border-zinc-800">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex-1 rounded-none h-12 text-zinc-400 hover:text-white hover:bg-zinc-800 focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none"
|
||||
>
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
Configure
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="bg-zinc-900 border-zinc-700"
|
||||
side="bottom"
|
||||
align="end"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="text-white hover:bg-zinc-800 cursor-pointer"
|
||||
onClick={handleTestA2A}
|
||||
>
|
||||
<FlaskConical className="h-4 w-4 mr-2 text-emerald-400" />
|
||||
Test A2A
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-white hover:bg-zinc-800 cursor-pointer"
|
||||
onClick={() => onEdit(agent)}
|
||||
>
|
||||
<Pencil className="h-4 w-4 mr-2 text-emerald-400" />
|
||||
Edit Agent
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-white hover:bg-zinc-800 cursor-pointer"
|
||||
onClick={() => onMove(agent)}
|
||||
>
|
||||
<MoveRight className="h-4 w-4 mr-2 text-yellow-400" />
|
||||
Move Agent
|
||||
</DropdownMenuItem>
|
||||
{onWorkflow && agent.type === "workflow" && (
|
||||
<DropdownMenuItem
|
||||
className="text-white hover:bg-zinc-800 cursor-pointer"
|
||||
onClick={() => onWorkflow(agent.id)}
|
||||
>
|
||||
<Workflow className="h-4 w-4 mr-2 text-blue-400" />
|
||||
Open Workflow
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="text-white hover:bg-zinc-800 cursor-pointer"
|
||||
onClick={handleExportAgent}
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2 text-purple-400" />
|
||||
Export as JSON
|
||||
</DropdownMenuItem>
|
||||
{onShare && (
|
||||
<DropdownMenuItem
|
||||
className="text-white hover:bg-zinc-800 cursor-pointer"
|
||||
onClick={() => onShare(agent)}
|
||||
>
|
||||
<Share2 className="h-4 w-4 mr-2 text-blue-400" />
|
||||
Share Agent
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="text-red-500 hover:bg-zinc-800 cursor-pointer"
|
||||
onClick={() => onDelete(agent)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete Agent
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="w-px bg-zinc-800" />
|
||||
<a
|
||||
href={agent.agent_card_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
"flex-1 flex items-center justify-center rounded-none h-12 hover:bg-zinc-800",
|
||||
{
|
||||
"text-green-400 hover:text-green-300": agent.type === "llm",
|
||||
"text-indigo-400 hover:text-indigo-300": agent.type === "a2a",
|
||||
"text-yellow-400 hover:text-yellow-300":
|
||||
agent.type === "sequential",
|
||||
"text-purple-400 hover:text-purple-300":
|
||||
agent.type === "parallel",
|
||||
"text-orange-400 hover:text-orange-300": agent.type === "loop",
|
||||
"text-blue-400 hover:text-blue-300": agent.type === "workflow",
|
||||
"text-red-400 hover:text-red-300": agent.type === "task",
|
||||
}
|
||||
)}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 mr-2" />
|
||||
Agent Card
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
130
frontend/app/agents/AgentList.tsx
Normal file
130
frontend/app/agents/AgentList.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/AgentList.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Agent } from "@/types/agent";
|
||||
import { MCPServer } from "@/types/mcpServer";
|
||||
import { AgentCard } from "./AgentCard";
|
||||
import { EmptyState } from "./EmptyState";
|
||||
import { ApiKey, Folder } from "@/services/agentService";
|
||||
|
||||
interface AgentListProps {
|
||||
agents: Agent[];
|
||||
isLoading: boolean;
|
||||
searchTerm: string;
|
||||
selectedFolderId: string | null;
|
||||
availableMCPs: MCPServer[];
|
||||
getApiKeyNameById: (id: string | undefined) => string | null;
|
||||
getAgentNameById: (id: string) => string;
|
||||
onEdit: (agent: Agent) => void;
|
||||
onDelete: (agent: Agent) => void;
|
||||
onMove: (agent: Agent) => void;
|
||||
onShare?: (agent: Agent) => void;
|
||||
onWorkflow?: (agentId: string) => void;
|
||||
onClearSearch?: () => void;
|
||||
onCreateAgent?: () => void;
|
||||
apiKeys: ApiKey[];
|
||||
folders: Folder[];
|
||||
}
|
||||
|
||||
export function AgentList({
|
||||
agents,
|
||||
isLoading,
|
||||
searchTerm,
|
||||
selectedFolderId,
|
||||
availableMCPs,
|
||||
getApiKeyNameById,
|
||||
getAgentNameById,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onMove,
|
||||
onShare,
|
||||
onWorkflow,
|
||||
onClearSearch,
|
||||
onCreateAgent,
|
||||
apiKeys,
|
||||
folders,
|
||||
}: AgentListProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[60vh]">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-emerald-400"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (agents.length === 0) {
|
||||
if (searchTerm) {
|
||||
return (
|
||||
<EmptyState
|
||||
type="search-no-results"
|
||||
searchTerm={searchTerm}
|
||||
onAction={onClearSearch}
|
||||
/>
|
||||
);
|
||||
} else if (selectedFolderId) {
|
||||
return (
|
||||
<EmptyState
|
||||
type="empty-folder"
|
||||
onAction={onCreateAgent}
|
||||
actionLabel="Create Agent"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<EmptyState
|
||||
type="no-agents"
|
||||
onAction={onCreateAgent}
|
||||
actionLabel="Create Agent"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{agents.map((agent) => (
|
||||
<AgentCard
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onMove={onMove}
|
||||
onShare={onShare}
|
||||
onWorkflow={onWorkflow}
|
||||
availableMCPs={availableMCPs}
|
||||
getApiKeyNameById={getApiKeyNameById}
|
||||
getAgentNameById={getAgentNameById}
|
||||
folders={folders}
|
||||
agents={agents}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
186
frontend/app/agents/AgentSidebar.tsx
Normal file
186
frontend/app/agents/AgentSidebar.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/AgentSidebar.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Folder,
|
||||
FolderPlus,
|
||||
Home,
|
||||
X,
|
||||
CircleEllipsis,
|
||||
Edit,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
|
||||
interface AgentFolder {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface AgentSidebarProps {
|
||||
visible: boolean;
|
||||
folders: AgentFolder[];
|
||||
selectedFolderId: string | null;
|
||||
onSelectFolder: (id: string | null) => void;
|
||||
onAddFolder: () => void;
|
||||
onEditFolder: (folder: AgentFolder) => void;
|
||||
onDeleteFolder: (folder: AgentFolder) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function AgentSidebar({
|
||||
visible,
|
||||
folders,
|
||||
selectedFolderId,
|
||||
onSelectFolder,
|
||||
onAddFolder,
|
||||
onEditFolder,
|
||||
onDeleteFolder,
|
||||
onClose,
|
||||
}: AgentSidebarProps) {
|
||||
return (
|
||||
<>
|
||||
{visible && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute right-4 top-4 z-40 bg-[#222] p-2 rounded-md text-emerald-400 hover:bg-[#333] hover:text-emerald-400 shadow-md transition-all"
|
||||
aria-label="Hide folders"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`fixed top-0 z-30 h-full w-64 bg-[#1a1a1a] p-4 shadow-xl transition-all duration-300 ease-in-out ${
|
||||
visible ? "left-64 translate-x-0" : "left-0 -translate-x-full pointer-events-none"
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-semibold text-white flex items-center">
|
||||
<Folder className="h-5 w-5 mr-2 text-emerald-400" />
|
||||
Folders
|
||||
</h2>
|
||||
<div className="flex space-x-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0 text-neutral-400 hover:text-emerald-400 hover:bg-[#222]"
|
||||
onClick={onAddFolder}
|
||||
>
|
||||
<FolderPlus className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0 text-neutral-400 hover:text-emerald-400 hover:bg-[#222]"
|
||||
onClick={onClose}
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<button
|
||||
className={`w-full text-left px-3 py-2 rounded-md flex items-center ${
|
||||
selectedFolderId === null
|
||||
? "bg-[#333] text-emerald-400"
|
||||
: "text-neutral-300 hover:bg-[#222] hover:text-white"
|
||||
}`}
|
||||
onClick={() => onSelectFolder(null)}
|
||||
>
|
||||
<Home className="h-4 w-4 mr-2" />
|
||||
<span>All agents</span>
|
||||
</button>
|
||||
|
||||
{folders.map((folder) => (
|
||||
<div key={folder.id} className="flex items-center group">
|
||||
<button
|
||||
className={`flex-1 text-left px-3 py-2 rounded-md flex items-center ${
|
||||
selectedFolderId === folder.id
|
||||
? "bg-[#333] text-emerald-400"
|
||||
: "text-neutral-300 hover:bg-[#222] hover:text-white"
|
||||
}`}
|
||||
onClick={() => onSelectFolder(folder.id)}
|
||||
>
|
||||
<Folder className="h-4 w-4 mr-2" />
|
||||
<span className="truncate">{folder.name}</span>
|
||||
</button>
|
||||
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 p-0 text-neutral-400 hover:text-white hover:bg-[#222]"
|
||||
>
|
||||
<CircleEllipsis className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="bg-[#222] border-[#333] text-white"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer hover:bg-[#333] focus:bg-[#333]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEditFolder(folder);
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer text-red-500 hover:bg-[#333] hover:text-red-400 focus:bg-[#333]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteFolder(folder);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
96
frontend/app/agents/AgentTypeSelector.tsx
Normal file
96
frontend/app/agents/AgentTypeSelector.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/AgentTypeSelector.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { AgentType } from "@/types/agent";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Code,
|
||||
ExternalLink,
|
||||
GitBranch,
|
||||
RefreshCw,
|
||||
Workflow,
|
||||
Users,
|
||||
BookOpenCheck,
|
||||
} from "lucide-react";
|
||||
|
||||
interface AgentTypeSelectorProps {
|
||||
value: AgentType;
|
||||
onValueChange: (value: AgentType) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const agentTypes = [
|
||||
{ value: "llm", label: "LLM Agent", icon: Code },
|
||||
{ value: "a2a", label: "A2A Agent", icon: ExternalLink },
|
||||
{ value: "sequential", label: "Sequential Agent", icon: Workflow },
|
||||
{ value: "parallel", label: "Parallel Agent", icon: GitBranch },
|
||||
{ value: "loop", label: "Loop Agent", icon: RefreshCw },
|
||||
{ value: "workflow", label: "Workflow Agent", icon: Workflow },
|
||||
{ value: "task", label: "Task Agent", icon: BookOpenCheck },
|
||||
];
|
||||
|
||||
export function AgentTypeSelector({
|
||||
value,
|
||||
onValueChange,
|
||||
className = "",
|
||||
}: AgentTypeSelectorProps) {
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(value: AgentType) => onValueChange(value)}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={`bg-[#222] border-[#444] text-white ${className}`}
|
||||
>
|
||||
<SelectValue placeholder="Select type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#222] border-[#444] text-white">
|
||||
{agentTypes.map((type) => (
|
||||
<SelectItem
|
||||
key={type.value}
|
||||
value={type.value}
|
||||
className="data-[selected]:bg-[#333] data-[highlighted]:bg-[#333] text-white focus:!text-white hover:text-emerald-400 data-[selected]:!text-emerald-400"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<type.icon className="h-4 w-4 text-emerald-400" />
|
||||
{type.label}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
107
frontend/app/agents/EmptyState.tsx
Normal file
107
frontend/app/agents/EmptyState.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/EmptyState.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Folder, Plus, Search, Server } from "lucide-react";
|
||||
|
||||
interface EmptyStateProps {
|
||||
type: "no-agents" | "empty-folder" | "search-no-results";
|
||||
searchTerm?: string;
|
||||
onAction?: () => void;
|
||||
actionLabel?: string;
|
||||
}
|
||||
|
||||
export function EmptyState({
|
||||
type,
|
||||
searchTerm = "",
|
||||
onAction,
|
||||
actionLabel = "Create Agent",
|
||||
}: EmptyStateProps) {
|
||||
const getIcon = () => {
|
||||
switch (type) {
|
||||
case "empty-folder":
|
||||
return <Folder className="h-16 w-16 text-emerald-400" />;
|
||||
case "search-no-results":
|
||||
return <Search className="h-16 w-16 text-emerald-400" />;
|
||||
case "no-agents":
|
||||
default:
|
||||
return <Server className="h-16 w-16 text-emerald-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getTitle = () => {
|
||||
switch (type) {
|
||||
case "empty-folder":
|
||||
return "Empty folder";
|
||||
case "search-no-results":
|
||||
return "No agents found";
|
||||
case "no-agents":
|
||||
default:
|
||||
return "No agents found";
|
||||
}
|
||||
};
|
||||
|
||||
const getMessage = () => {
|
||||
switch (type) {
|
||||
case "empty-folder":
|
||||
return "This folder is empty. Add agents or create a new one.";
|
||||
case "search-no-results":
|
||||
return `We couldn't find any agents that match your search: "${searchTerm}"`;
|
||||
case "no-agents":
|
||||
default:
|
||||
return "You don't have any agents configured. Create your first agent to start!";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-[60vh] text-center">
|
||||
<div className="mb-6 p-8 rounded-full bg-[#1a1a1a] border border-[#333]">
|
||||
{getIcon()}
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold text-white mb-3">{getTitle()}</h2>
|
||||
<p className="text-neutral-300 mb-6 max-w-md">{getMessage()}</p>
|
||||
{onAction && (
|
||||
<Button
|
||||
onClick={onAction}
|
||||
className={
|
||||
type === "search-no-results"
|
||||
? "bg-[#222] text-white hover:bg-[#333]"
|
||||
: "bg-emerald-400 text-black hover:bg-[#00cc7d] px-6 py-2 hover:shadow-[0_0_15px_rgba(0,255,157,0.2)]"
|
||||
}
|
||||
>
|
||||
{type === "search-no-results" ? null : (
|
||||
<Plus className="mr-2 h-5 w-5" />
|
||||
)}
|
||||
{type === "search-no-results" ? "Clear search" : actionLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
153
frontend/app/agents/SearchInput.tsx
Normal file
153
frontend/app/agents/SearchInput.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/SearchInput.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Search, X, Filter } from "lucide-react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
|
||||
interface SearchInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
selectedAgentType?: string | null;
|
||||
onAgentTypeChange?: (type: string | null) => void;
|
||||
agentTypes?: string[];
|
||||
}
|
||||
|
||||
// Using "all" as a special value to represent no filter
|
||||
const ANY_TYPE_VALUE = "all";
|
||||
|
||||
export function SearchInput({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "Search agents...",
|
||||
className = "",
|
||||
selectedAgentType = null,
|
||||
onAgentTypeChange,
|
||||
agentTypes = [],
|
||||
}: SearchInputProps) {
|
||||
const [isFilterOpen, setIsFilterOpen] = useState(false);
|
||||
|
||||
const handleTypeChange = (value: string) => {
|
||||
if (onAgentTypeChange) {
|
||||
onAgentTypeChange(value === ANY_TYPE_VALUE ? null : value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`relative flex items-center gap-2 ${className}`}>
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-neutral-400" />
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
autoComplete="off"
|
||||
className="pl-10 w-full bg-[#222] border-[#444] text-white focus:border-emerald-400 focus:ring-emerald-400/10"
|
||||
/>
|
||||
{value && (
|
||||
<button
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white"
|
||||
onClick={() => onChange("")}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{agentTypes.length > 0 && onAgentTypeChange && (
|
||||
<Popover open={isFilterOpen} onOpenChange={setIsFilterOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={`px-3 py-2 bg-[#222] border-[#444] hover:bg-[#333] ${
|
||||
selectedAgentType ? "text-emerald-400" : "text-neutral-400"
|
||||
}`}
|
||||
>
|
||||
<Filter className="h-4 w-4 mr-1" />
|
||||
{selectedAgentType ? "Filtered" : "Filter"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-48 p-2 bg-[#222] border-[#444]">
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs font-medium text-neutral-300">
|
||||
Filter by type
|
||||
</div>
|
||||
<Select
|
||||
value={selectedAgentType ? selectedAgentType : ANY_TYPE_VALUE}
|
||||
onValueChange={handleTypeChange}
|
||||
>
|
||||
<SelectTrigger className="bg-[#333] border-[#444] text-white">
|
||||
<SelectValue placeholder="Any type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#333] border-[#444] text-white">
|
||||
<SelectItem value={ANY_TYPE_VALUE}>Any type</SelectItem>
|
||||
{agentTypes.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{type}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{selectedAgentType && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full text-xs text-neutral-300 hover:text-white"
|
||||
onClick={() => {
|
||||
onAgentTypeChange(null);
|
||||
setIsFilterOpen(false);
|
||||
}}
|
||||
>
|
||||
Clear filter
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
73
frontend/app/agents/config/A2AAgentConfig.tsx
Normal file
73
frontend/app/agents/config/A2AAgentConfig.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/config/A2AAgentConfig.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
interface A2AAgentConfigProps {
|
||||
values: {
|
||||
agent_card_url?: string;
|
||||
};
|
||||
onChange: (values: any) => void;
|
||||
}
|
||||
|
||||
export function A2AAgentConfig({ values, onChange }: A2AAgentConfigProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="agent_card_url" className="text-right text-neutral-300">
|
||||
Agent Card URL
|
||||
</Label>
|
||||
<Input
|
||||
id="agent_card_url"
|
||||
value={values.agent_card_url || ""}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...values,
|
||||
agent_card_url: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="https://example.com/.well-known/agent-card.json"
|
||||
className="col-span-3 bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="pl-[25%] text-sm text-neutral-400">
|
||||
<p>
|
||||
Provide the full URL for the JSON file of the Agent Card that describes
|
||||
this agent.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Agent Cards contain metadata, capabilities descriptions and supported
|
||||
protocols.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
367
frontend/app/agents/config/LLMAgentConfig.tsx
Normal file
367
frontend/app/agents/config/LLMAgentConfig.tsx
Normal file
@@ -0,0 +1,367 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/config/LLMAgentConfig.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ApiKey } from "@/services/agentService";
|
||||
import { Plus, Maximize2, Save } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
interface ModelOption {
|
||||
value: string;
|
||||
label: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
interface LLMAgentConfigProps {
|
||||
apiKeys: ApiKey[];
|
||||
availableModels: ModelOption[];
|
||||
values: {
|
||||
model?: string;
|
||||
api_key_id?: string;
|
||||
instruction?: string;
|
||||
role?: string;
|
||||
goal?: string;
|
||||
};
|
||||
onChange: (values: any) => void;
|
||||
onOpenApiKeysDialog: () => void;
|
||||
}
|
||||
|
||||
export function LLMAgentConfig({
|
||||
apiKeys,
|
||||
availableModels,
|
||||
values,
|
||||
onChange,
|
||||
onOpenApiKeysDialog,
|
||||
}: LLMAgentConfigProps) {
|
||||
const [instructionText, setInstructionText] = useState(values.instruction || "");
|
||||
const [isInstructionModalOpen, setIsInstructionModalOpen] = useState(false);
|
||||
const [expandedInstructionText, setExpandedInstructionText] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setInstructionText(values.instruction || "");
|
||||
}, [values.instruction]);
|
||||
|
||||
const handleInstructionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newValue = e.target.value;
|
||||
setInstructionText(newValue);
|
||||
|
||||
onChange({
|
||||
...values,
|
||||
instruction: newValue,
|
||||
});
|
||||
};
|
||||
|
||||
const handleExpandInstruction = () => {
|
||||
setExpandedInstructionText(instructionText);
|
||||
setIsInstructionModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveExpandedInstruction = () => {
|
||||
setInstructionText(expandedInstructionText);
|
||||
onChange({
|
||||
...values,
|
||||
instruction: expandedInstructionText,
|
||||
});
|
||||
setIsInstructionModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="role" className="text-right text-neutral-300">
|
||||
Role
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
<Input
|
||||
id="role"
|
||||
value={values.role || ""}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...values,
|
||||
role: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="Ex: Research Assistant, Customer Support, etc."
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
<div className="mt-1 text-xs text-neutral-400">
|
||||
<span className="inline-block h-3 w-3 mr-1">ℹ️</span>
|
||||
<span>Define the role or persona that the agent will assume</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="goal" className="text-right text-neutral-300">
|
||||
Goal
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
<Input
|
||||
id="goal"
|
||||
value={values.goal || ""}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...values,
|
||||
goal: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="Ex: Find and organize information, Assist customers with inquiries, etc."
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
<div className="mt-1 text-xs text-neutral-400">
|
||||
<span className="inline-block h-3 w-3 mr-1">ℹ️</span>
|
||||
<span>Define the main objective or purpose of this agent</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="api_key" className="text-right text-neutral-300">
|
||||
API Key
|
||||
</Label>
|
||||
<div className="col-span-3 space-y-4">
|
||||
<div className="flex items-center">
|
||||
<Select
|
||||
value={values.api_key_id || ""}
|
||||
onValueChange={(value) =>
|
||||
onChange({
|
||||
...values,
|
||||
api_key_id: value,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="flex-1 bg-[#222] border-[#444] text-white">
|
||||
<SelectValue placeholder="Select an API key" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#222] border-[#444] text-white">
|
||||
{apiKeys.length > 0 ? (
|
||||
apiKeys
|
||||
.filter((key) => key.is_active !== false)
|
||||
.map((key) => (
|
||||
<SelectItem
|
||||
key={key.id}
|
||||
value={key.id}
|
||||
className="data-[selected]:bg-[#333] data-[highlighted]:bg-[#333] !text-white focus:!text-white hover:text-emerald-400 data-[selected]:!text-emerald-400"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span>{key.name}</span>
|
||||
<Badge className="ml-2 bg-[#333] text-emerald-400 text-xs">
|
||||
{key.provider}
|
||||
</Badge>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<div className="text-neutral-500 px-2 py-1.5 pl-8">
|
||||
No API keys available
|
||||
</div>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onOpenApiKeysDialog}
|
||||
className="ml-2 bg-[#222] text-emerald-400 hover:bg-[#333]"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{apiKeys.length === 0 && (
|
||||
<div className="flex items-center text-xs text-neutral-400">
|
||||
<span className="inline-block h-3 w-3 mr-1 text-neutral-400">i</span>
|
||||
<span>
|
||||
You need to{" "}
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={onOpenApiKeysDialog}
|
||||
className="h-auto p-0 text-xs text-emerald-400"
|
||||
>
|
||||
register API keys
|
||||
</Button>{" "}
|
||||
before creating an agent.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="model" className="text-right text-neutral-300">
|
||||
Model
|
||||
</Label>
|
||||
<Select
|
||||
value={values.model}
|
||||
onValueChange={(value) =>
|
||||
onChange({
|
||||
...values,
|
||||
model: value,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="col-span-3 bg-[#222] border-[#444] text-white">
|
||||
<SelectValue placeholder="Select the model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#222] border-[#444] text-white p-0">
|
||||
<div className="sticky top-0 z-10 p-2 bg-[#222] border-b border-[#444]">
|
||||
<Input
|
||||
placeholder="Search models..."
|
||||
className="bg-[#333] border-[#444] text-white h-8"
|
||||
onChange={(e) => {
|
||||
const searchQuery = e.target.value.toLowerCase();
|
||||
const items = document.querySelectorAll('[data-model-item="true"]');
|
||||
items.forEach((item) => {
|
||||
const text = item.textContent?.toLowerCase() || '';
|
||||
if (text.includes(searchQuery)) {
|
||||
(item as HTMLElement).style.display = 'flex';
|
||||
} else {
|
||||
(item as HTMLElement).style.display = 'none';
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-[200px] overflow-y-auto py-1">
|
||||
{availableModels
|
||||
.filter((model) => {
|
||||
if (!values.api_key_id) return true;
|
||||
|
||||
const selectedKey = apiKeys.find(
|
||||
(key) => key.id === values.api_key_id
|
||||
);
|
||||
|
||||
if (!selectedKey) return true;
|
||||
|
||||
return model.provider === selectedKey.provider;
|
||||
})
|
||||
.map((model) => (
|
||||
<SelectItem
|
||||
key={model.value}
|
||||
value={model.value}
|
||||
className="data-[selected]:bg-[#333] data-[highlighted]:bg-[#333] !text-white focus:!text-white hover:text-emerald-400 data-[selected]:!text-emerald-400"
|
||||
data-model-item="true"
|
||||
>
|
||||
{model.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="instruction" className="text-right text-neutral-300">
|
||||
Instructions
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
id="instruction"
|
||||
value={instructionText}
|
||||
onChange={handleInstructionChange}
|
||||
className="w-full bg-[#222] border-[#444] text-white pr-10"
|
||||
rows={4}
|
||||
onClick={handleExpandInstruction}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-3 right-5 text-neutral-400 hover:text-emerald-400 focus:outline-none"
|
||||
onClick={handleExpandInstruction}
|
||||
>
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-neutral-400">
|
||||
<span className="inline-block h-3 w-3 mr-1">ℹ️</span>
|
||||
<span>
|
||||
Characters like {"{"} and {"}"} or {"{{"} and {"}}"} are automatically escaped to avoid errors in Python.
|
||||
<span className="ml-2 text-emerald-400">Click to expand editor.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Instruction Modal */}
|
||||
<Dialog open={isInstructionModalOpen} onOpenChange={setIsInstructionModalOpen}>
|
||||
<DialogContent className="sm:max-w-[1200px] max-h-[90vh] bg-[#1a1a1a] border-[#333] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Agent Instructions</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-hidden flex flex-col min-h-[60vh]">
|
||||
<Textarea
|
||||
value={expandedInstructionText}
|
||||
onChange={(e) => setExpandedInstructionText(e.target.value)}
|
||||
className="flex-1 min-h-full bg-[#222] border-[#444] text-white p-4 focus:border-emerald-400 focus:ring-emerald-400 focus:ring-opacity-50 resize-none"
|
||||
placeholder="Enter detailed instructions for the agent..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsInstructionModalOpen(false)}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveExpandedInstruction}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Save Instructions
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
133
frontend/app/agents/config/LoopAgentConfig copy.tsx
Normal file
133
frontend/app/agents/config/LoopAgentConfig copy.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/config/LoopAgentConfig.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
|
||||
interface LoopAgentConfigProps {
|
||||
values: {
|
||||
config?: {
|
||||
sub_agents?: string[];
|
||||
max_iterations?: number;
|
||||
};
|
||||
};
|
||||
onChange: (values: any) => void;
|
||||
agents: Agent[];
|
||||
getAgentNameById: (id: string) => string;
|
||||
}
|
||||
|
||||
export function LoopAgentConfig({
|
||||
values,
|
||||
onChange,
|
||||
agents,
|
||||
getAgentNameById,
|
||||
}: LoopAgentConfigProps) {
|
||||
const handleMaxIterationsChange = (value: string) => {
|
||||
const maxIterations = parseInt(value);
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...values.config,
|
||||
max_iterations: isNaN(maxIterations) ? undefined : maxIterations,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="max_iterations" className="text-right text-neutral-300">
|
||||
Max. Iterations
|
||||
</Label>
|
||||
<Input
|
||||
id="max_iterations"
|
||||
type="number"
|
||||
min={1}
|
||||
max={100}
|
||||
value={values.config?.max_iterations || ""}
|
||||
onChange={(e) => handleMaxIterationsChange(e.target.value)}
|
||||
className="col-span-3 bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||
<h3 className="text-sm font-medium text-white mb-4">
|
||||
Execution Order of Agents
|
||||
</h3>
|
||||
|
||||
{values.config?.sub_agents && values.config.sub_agents.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{values.config.sub_agents.map((agentId, index) => (
|
||||
<div
|
||||
key={agentId}
|
||||
className="flex items-center space-x-2 bg-[#2a2a2a] p-3 rounded-md"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-white">
|
||||
{getAgentNameById(agentId)}
|
||||
</div>
|
||||
<div className="text-sm text-neutral-400">
|
||||
Executed on{" "}
|
||||
<Badge className="bg-[#333] text-emerald-400 border-none">
|
||||
Position {index + 1}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-6 text-neutral-400">
|
||||
Add agents in the "Sub-Agents" tab to define the execution order
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3 text-sm text-neutral-400">
|
||||
<p>
|
||||
The agents will be executed sequentially in the order listed above.
|
||||
The output of each agent will be provided as input to the next
|
||||
agent in the sequence.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
125
frontend/app/agents/config/ParallelAgentConfig.tsx
Normal file
125
frontend/app/agents/config/ParallelAgentConfig.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/config/ParallelAgentConfig.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { GitBranch } from "lucide-react";
|
||||
|
||||
interface ParallelAgentConfigProps {
|
||||
values: {
|
||||
config?: {
|
||||
sub_agents?: string[];
|
||||
aggregation_method?: string;
|
||||
timeout_seconds?: number;
|
||||
custom_mcp_servers?: any[];
|
||||
wait_for_all?: boolean;
|
||||
};
|
||||
};
|
||||
onChange: (values: any) => void;
|
||||
agents: Agent[];
|
||||
getAgentNameById: (id: string) => string;
|
||||
}
|
||||
|
||||
export function ParallelAgentConfig({
|
||||
values,
|
||||
onChange,
|
||||
agents,
|
||||
getAgentNameById,
|
||||
}: ParallelAgentConfigProps) {
|
||||
const aggregationMethods = [
|
||||
{ value: "merge", label: "Merge all responses" },
|
||||
{ value: "first", label: "Use only the first response" },
|
||||
{ value: "last", label: "Use only the last response" },
|
||||
{ value: "custom", label: "Custom aggregation" },
|
||||
];
|
||||
|
||||
const handleTimeoutChange = (value: string) => {
|
||||
const timeout = parseInt(value);
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...values.config,
|
||||
timeout_seconds: isNaN(timeout) ? undefined : timeout,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||
<h3 className="text-sm font-medium text-white mb-4">
|
||||
Agents in Parallel
|
||||
</h3>
|
||||
|
||||
{values.config?.sub_agents && values.config.sub_agents.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{values.config.sub_agents.map((agentId) => (
|
||||
<div
|
||||
key={agentId}
|
||||
className="flex items-center space-x-2 bg-[#2a2a2a] p-3 rounded-md"
|
||||
>
|
||||
<GitBranch className="h-5 w-5 text-emerald-400" />
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-white truncate">
|
||||
{getAgentNameById(agentId)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-6 text-neutral-400">
|
||||
Add agents in the "Sub-Agents" tab to execute in parallel
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3 text-sm text-neutral-400">
|
||||
<p>
|
||||
All listed agents will be executed simultaneously with the same
|
||||
input. The responses will be aggregated according to the selected
|
||||
method.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
120
frontend/app/agents/config/SequentialAgentConfig.tsx
Normal file
120
frontend/app/agents/config/SequentialAgentConfig.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/config/SequentialAgentConfig.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
|
||||
interface SequentialAgentConfigProps {
|
||||
values: {
|
||||
config?: {
|
||||
sub_agents?: string[];
|
||||
max_iterations?: number;
|
||||
custom_mcp_servers?: any[];
|
||||
};
|
||||
};
|
||||
onChange: (values: any) => void;
|
||||
agents: Agent[];
|
||||
getAgentNameById: (id: string) => string;
|
||||
}
|
||||
|
||||
export function SequentialAgentConfig({
|
||||
values,
|
||||
onChange,
|
||||
agents,
|
||||
getAgentNameById,
|
||||
}: SequentialAgentConfigProps) {
|
||||
const handleMaxIterationsChange = (value: string) => {
|
||||
const maxIterations = parseInt(value);
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...values.config,
|
||||
max_iterations: isNaN(maxIterations) ? undefined : maxIterations,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||
<h3 className="text-sm font-medium text-white mb-4">
|
||||
Execution Order of Agents
|
||||
</h3>
|
||||
|
||||
{values.config?.sub_agents && values.config.sub_agents.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{values.config.sub_agents.map((agentId, index) => (
|
||||
<div
|
||||
key={agentId}
|
||||
className="flex items-center space-x-2 bg-[#2a2a2a] p-3 rounded-md"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-white">
|
||||
{getAgentNameById(agentId)}
|
||||
</div>
|
||||
<div className="text-sm text-neutral-400">
|
||||
Executed on{" "}
|
||||
<Badge className="bg-[#333] text-emerald-400 border-none">
|
||||
Position {index + 1}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-6 text-neutral-400">
|
||||
Add agents in the "Sub-Agents" tab to define the execution order
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3 text-sm text-neutral-400">
|
||||
<p>
|
||||
The agents will be executed sequentially in the order listed above.
|
||||
The output of each agent will be provided as input to the next
|
||||
agent in the sequence.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
801
frontend/app/agents/config/TaskAgentConfig.tsx
Normal file
801
frontend/app/agents/config/TaskAgentConfig.tsx
Normal file
@@ -0,0 +1,801 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/config/TaskAgentConfig.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Agent, TaskConfig } from "@/types/agent";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Maximize2,
|
||||
Save,
|
||||
X,
|
||||
ArrowDown,
|
||||
List,
|
||||
Search,
|
||||
Edit,
|
||||
PenTool,
|
||||
} from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
|
||||
interface TaskAgentConfigProps {
|
||||
values: Partial<Agent>;
|
||||
onChange: (values: Partial<Agent>) => void;
|
||||
agents: Agent[];
|
||||
getAgentNameById: (id: string) => string;
|
||||
singleTask?: boolean;
|
||||
}
|
||||
|
||||
const getAgentTypeLabel = (type: string): string => {
|
||||
const typeMap: Record<string, string> = {
|
||||
llm: "LLM",
|
||||
a2a: "A2A",
|
||||
sequential: "Sequential",
|
||||
parallel: "Parallel",
|
||||
loop: "Loop",
|
||||
workflow: "Workflow",
|
||||
task: "Task",
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
};
|
||||
|
||||
const getAgentTypeColor = (type: string): string => {
|
||||
const colorMap: Record<string, string> = {
|
||||
llm: "bg-blue-800 text-white",
|
||||
a2a: "bg-purple-800 text-white",
|
||||
sequential: "bg-orange-800 text-white",
|
||||
parallel: "bg-green-800 text-white",
|
||||
loop: "bg-pink-800 text-white",
|
||||
workflow: "bg-yellow-800 text-black",
|
||||
task: "bg-green-800 text-white",
|
||||
};
|
||||
return colorMap[type] || "bg-neutral-800 text-white";
|
||||
};
|
||||
|
||||
export function TaskAgentConfig({
|
||||
values,
|
||||
onChange,
|
||||
agents,
|
||||
getAgentNameById,
|
||||
singleTask = false,
|
||||
}: TaskAgentConfigProps) {
|
||||
const [newTask, setNewTask] = useState<TaskConfig>({
|
||||
agent_id: "",
|
||||
description: "",
|
||||
expected_output: "",
|
||||
enabled_tools: [],
|
||||
});
|
||||
|
||||
const [taskAgentSearchQuery, setTaskAgentSearchQuery] = useState<string>("");
|
||||
const [filteredTaskAgents, setFilteredTaskAgents] = useState<Agent[]>([]);
|
||||
const [isDescriptionModalOpen, setIsDescriptionModalOpen] = useState(false);
|
||||
const [expandedDescription, setExpandedDescription] = useState("");
|
||||
const [editingTaskIndex, setEditingTaskIndex] = useState<number | null>(null);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const [toolSearchQuery, setToolSearchQuery] = useState<string>("");
|
||||
const [filteredTools, setFilteredTools] = useState<{id: string, name: string}[]>([]);
|
||||
const [isToolsModalOpen, setIsToolsModalOpen] = useState(false);
|
||||
const [tempSelectedTools, setTempSelectedTools] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isToolsModalOpen) {
|
||||
if (isEditing && editingTaskIndex !== null && values.config?.tasks) {
|
||||
setTempSelectedTools(
|
||||
values.config.tasks[editingTaskIndex]?.enabled_tools || []
|
||||
);
|
||||
} else {
|
||||
setTempSelectedTools([...newTask.enabled_tools || []]);
|
||||
}
|
||||
}
|
||||
}, [isToolsModalOpen]);
|
||||
|
||||
const getAvailableTaskAgents = (currentTaskAgentId?: string) =>
|
||||
agents.filter(
|
||||
(agent) =>
|
||||
agent.id !== values.id &&
|
||||
(!values.config?.tasks?.some((task) => task.agent_id === agent.id) ||
|
||||
agent.id === currentTaskAgentId)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const currentTaskAgentId =
|
||||
isEditing && editingTaskIndex !== null && values.config?.tasks
|
||||
? values.config.tasks[editingTaskIndex].agent_id
|
||||
: undefined;
|
||||
|
||||
const availableAgents = getAvailableTaskAgents(currentTaskAgentId);
|
||||
|
||||
if (taskAgentSearchQuery.trim() === "") {
|
||||
setFilteredTaskAgents(availableAgents);
|
||||
} else {
|
||||
const query = taskAgentSearchQuery.toLowerCase();
|
||||
setFilteredTaskAgents(
|
||||
availableAgents.filter(
|
||||
(agent) =>
|
||||
agent.name.toLowerCase().includes(query) ||
|
||||
(agent.description?.toLowerCase() || "").includes(query)
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [
|
||||
taskAgentSearchQuery,
|
||||
agents,
|
||||
values.config?.tasks,
|
||||
isEditing,
|
||||
editingTaskIndex,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset editing state when values change externally
|
||||
if (!isEditing) {
|
||||
const currentTaskAgentId =
|
||||
editingTaskIndex !== null && values.config?.tasks
|
||||
? values.config.tasks[editingTaskIndex]?.agent_id
|
||||
: undefined;
|
||||
setFilteredTaskAgents(getAvailableTaskAgents(currentTaskAgentId));
|
||||
}
|
||||
}, [agents, values.config?.tasks]);
|
||||
|
||||
const getAvailableTools = () => {
|
||||
if (!values.config?.tasks || values.config.tasks.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const taskAgentIds = values.config.tasks.map(task => task.agent_id);
|
||||
|
||||
const toolsList: {id: string, name: string}[] = [];
|
||||
const toolsMap: Record<string, boolean> = {};
|
||||
|
||||
taskAgentIds.forEach(agentId => {
|
||||
const agent = agents.find(a => a.id === agentId);
|
||||
|
||||
if (agent?.type === "llm" && agent.config?.tools) {
|
||||
agent.config.tools.forEach(tool => {
|
||||
if (!toolsMap[tool.id]) {
|
||||
toolsList.push({ id: tool.id, name: tool.id });
|
||||
toolsMap[tool.id] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (agent?.type === "llm" && agent.config?.mcp_servers) {
|
||||
agent.config.mcp_servers.forEach(mcp => {
|
||||
if (mcp.tools) {
|
||||
mcp.tools.forEach(toolId => {
|
||||
if (!toolsMap[toolId]) {
|
||||
toolsList.push({ id: toolId, name: toolId });
|
||||
toolsMap[toolId] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return toolsList;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const availableTools = getAvailableTools();
|
||||
|
||||
if (toolSearchQuery.trim() === "") {
|
||||
setFilteredTools(availableTools);
|
||||
} else {
|
||||
const query = toolSearchQuery.toLowerCase();
|
||||
setFilteredTools(
|
||||
availableTools.filter(
|
||||
(tool) =>
|
||||
tool.name.toLowerCase().includes(query) ||
|
||||
tool.id.toLowerCase().includes(query)
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [toolSearchQuery, values.config?.tasks, agents]);
|
||||
|
||||
const handleAddTask = () => {
|
||||
if (!newTask.agent_id || !newTask.description) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEditing && editingTaskIndex !== null) {
|
||||
const tasks = [...(values.config?.tasks || [])];
|
||||
tasks[editingTaskIndex] = { ...newTask };
|
||||
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
tasks,
|
||||
},
|
||||
});
|
||||
|
||||
setIsEditing(false);
|
||||
setEditingTaskIndex(null);
|
||||
} else {
|
||||
const tasks = [...(values.config?.tasks || [])];
|
||||
|
||||
if (singleTask) {
|
||||
tasks.splice(0, tasks.length, newTask);
|
||||
} else {
|
||||
tasks.push(newTask);
|
||||
}
|
||||
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
tasks,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setNewTask({
|
||||
agent_id: "",
|
||||
description: "",
|
||||
expected_output: "",
|
||||
enabled_tools: [],
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditTask = (index: number) => {
|
||||
const task = values.config?.tasks?.[index];
|
||||
if (task) {
|
||||
setNewTask({ ...task });
|
||||
setIsEditing(true);
|
||||
setEditingTaskIndex(index);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setNewTask({
|
||||
agent_id: "",
|
||||
description: "",
|
||||
expected_output: "",
|
||||
enabled_tools: [],
|
||||
});
|
||||
setIsEditing(false);
|
||||
setEditingTaskIndex(null);
|
||||
};
|
||||
|
||||
const handleRemoveTask = (index: number) => {
|
||||
if (editingTaskIndex === index) {
|
||||
handleCancelEdit();
|
||||
}
|
||||
|
||||
const tasks = [...(values.config?.tasks || [])];
|
||||
tasks.splice(index, 1);
|
||||
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
tasks,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDescriptionChange = (
|
||||
e: React.ChangeEvent<HTMLTextAreaElement>
|
||||
) => {
|
||||
const newValue = e.target.value;
|
||||
setNewTask({
|
||||
...newTask,
|
||||
description: newValue,
|
||||
});
|
||||
};
|
||||
|
||||
const handleExpandDescription = () => {
|
||||
setExpandedDescription(newTask.description);
|
||||
setIsDescriptionModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveExpandedDescription = () => {
|
||||
setNewTask({
|
||||
...newTask,
|
||||
description: expandedDescription,
|
||||
});
|
||||
setIsDescriptionModalOpen(false);
|
||||
};
|
||||
|
||||
const handleToggleTool = (toolId: string) => {
|
||||
const index = tempSelectedTools.indexOf(toolId);
|
||||
|
||||
if (index > -1) {
|
||||
setTempSelectedTools(tempSelectedTools.filter(id => id !== toolId));
|
||||
} else {
|
||||
setTempSelectedTools([...tempSelectedTools, toolId]);
|
||||
}
|
||||
};
|
||||
|
||||
const isToolEnabled = (toolId: string) => {
|
||||
return tempSelectedTools.includes(toolId);
|
||||
};
|
||||
|
||||
const handleSaveTools = () => {
|
||||
if (isEditing && editingTaskIndex !== null && values.config?.tasks) {
|
||||
const tasks = [...(values.config?.tasks || [])];
|
||||
|
||||
const updatedTask = {
|
||||
...tasks[editingTaskIndex],
|
||||
enabled_tools: [...tempSelectedTools]
|
||||
};
|
||||
|
||||
tasks[editingTaskIndex] = updatedTask;
|
||||
|
||||
const newConfig = {
|
||||
...(values.config || {}),
|
||||
tasks: tasks
|
||||
};
|
||||
|
||||
onChange({
|
||||
...values,
|
||||
config: newConfig
|
||||
});
|
||||
|
||||
} else if (newTask.agent_id) {
|
||||
const updatedNewTask = {
|
||||
...newTask,
|
||||
enabled_tools: [...tempSelectedTools]
|
||||
};
|
||||
|
||||
setNewTask(updatedNewTask);
|
||||
}
|
||||
|
||||
setIsToolsModalOpen(false);
|
||||
};
|
||||
|
||||
const renderAgentTypeBadge = (agentId: string) => {
|
||||
const agent = agents.find((a) => a.id === agentId);
|
||||
if (!agent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge className={`ml-2 ${getAgentTypeColor(agent.type)} text-xs`}>
|
||||
{getAgentTypeLabel(agent.type)}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-medium text-white flex items-center">
|
||||
<List className="mr-2 h-5 w-5 text-emerald-400" />
|
||||
{singleTask ? "Task" : "Tasks"}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||
<p className="text-sm text-neutral-400 mb-4">
|
||||
{singleTask
|
||||
? "Configure the task that will be executed by the agent."
|
||||
: "Configure the sequential tasks that will be executed by the team of agents."}
|
||||
</p>
|
||||
|
||||
{values.config?.tasks && values.config.tasks.length > 0 ? (
|
||||
<div className="space-y-4 mb-4">
|
||||
{values.config.tasks.map((task, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`border border-[#333] rounded-md p-3 ${
|
||||
editingTaskIndex === index ? "bg-[#1e3a3a]" : "bg-[#2a2a2a]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center">
|
||||
<span className="inline-flex items-center justify-center rounded-full bg-[#333] px-2 py-1 text-xs text-white mr-2">
|
||||
{index + 1}
|
||||
</span>
|
||||
<h4 className="font-medium text-white flex items-center">
|
||||
{getAgentNameById(task.agent_id)}
|
||||
{renderAgentTypeBadge(task.agent_id)}
|
||||
</h4>
|
||||
</div>
|
||||
<p className="text-sm text-neutral-300 mt-1">
|
||||
{task.description}
|
||||
</p>
|
||||
{task.expected_output && (
|
||||
<div className="mt-2">
|
||||
<span className="text-xs text-neutral-400">
|
||||
Expected output:
|
||||
</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="ml-2 bg-[#333] text-emerald-400 border-emerald-400/30"
|
||||
>
|
||||
{task.expected_output}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{task.enabled_tools && task.enabled_tools.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<span className="text-xs text-neutral-400">
|
||||
Enabled tools:
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{task.enabled_tools.map((toolId) => (
|
||||
<Badge
|
||||
key={toolId}
|
||||
className="bg-[#333] text-emerald-400 border border-emerald-400/30 text-xs"
|
||||
>
|
||||
{toolId}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditTask(index)}
|
||||
className="text-neutral-400 hover:text-emerald-400 hover:bg-[#333] mr-1"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveTask(index)}
|
||||
className="text-red-500 hover:text-red-400 hover:bg-[#333]"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{!singleTask &&
|
||||
index < (values.config?.tasks?.length || 0) - 1 && (
|
||||
<div className="flex justify-center my-2">
|
||||
<ArrowDown className="h-4 w-4 text-neutral-400" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-4 mb-4 bg-[#2a2a2a] rounded-md">
|
||||
<p className="text-neutral-400">No tasks configured</p>
|
||||
<p className="text-xs text-neutral-500">
|
||||
{singleTask
|
||||
? "Add a task to define the agent's behavior"
|
||||
: "Add tasks to define the workflow of the team"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!singleTask ||
|
||||
!values.config?.tasks ||
|
||||
values.config.tasks.length === 0 ||
|
||||
isEditing) && (
|
||||
<div className="space-y-3 border-t border-[#333] pt-4">
|
||||
<h4 className="text-sm font-medium text-white flex items-center justify-between">
|
||||
<span>
|
||||
{isEditing
|
||||
? "Edit task"
|
||||
: `Add ${singleTask ? "one" : "new"} task`}
|
||||
</span>
|
||||
{isEditing && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleCancelEdit}
|
||||
className="text-neutral-400 hover:text-emerald-400 hover:bg-[#333]"
|
||||
>
|
||||
<X className="h-4 w-4 mr-1" /> Cancel
|
||||
</Button>
|
||||
)}
|
||||
</h4>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="agent_id"
|
||||
className="text-xs text-neutral-400 mb-1 block"
|
||||
>
|
||||
Agent
|
||||
</Label>
|
||||
<Select
|
||||
value={newTask.agent_id}
|
||||
onValueChange={(value) =>
|
||||
setNewTask({ ...newTask, agent_id: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="bg-[#2a2a2a] border-[#444] text-white">
|
||||
<SelectValue placeholder="Select agent" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#2a2a2a] border-[#444] text-white p-0">
|
||||
<div className="sticky top-0 z-10 p-2 bg-[#2a2a2a] border-b border-[#444]">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-neutral-400" />
|
||||
<Input
|
||||
placeholder="Search agents..."
|
||||
className="bg-[#333] border-[#444] text-white h-8 pl-8"
|
||||
value={taskAgentSearchQuery}
|
||||
onChange={(e) =>
|
||||
setTaskAgentSearchQuery(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[200px] overflow-y-auto py-1">
|
||||
{filteredTaskAgents.length > 0 ? (
|
||||
filteredTaskAgents.map((agent) => (
|
||||
<SelectItem
|
||||
key={agent.id}
|
||||
value={agent.id}
|
||||
className="hover:bg-[#333] focus:bg-[#333] flex items-center justify-between px-2"
|
||||
data-agent-item="true"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">{agent.name}</span>
|
||||
<Badge
|
||||
className={`${getAgentTypeColor(
|
||||
agent.type
|
||||
)} text-xs`}
|
||||
>
|
||||
{getAgentTypeLabel(agent.type)}
|
||||
</Badge>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<div className="text-neutral-500 px-4 py-2 text-center">
|
||||
No agents found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<Label
|
||||
htmlFor="description"
|
||||
className="text-xs text-neutral-400 mb-1 block"
|
||||
>
|
||||
Task description
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
id="description"
|
||||
value={newTask.description}
|
||||
onChange={handleDescriptionChange}
|
||||
className="w-full bg-[#2a2a2a] border-[#444] text-white pr-10"
|
||||
rows={3}
|
||||
onClick={handleExpandDescription}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-3 right-5 text-neutral-400 hover:text-emerald-400 focus:outline-none"
|
||||
onClick={handleExpandDescription}
|
||||
>
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-neutral-400">
|
||||
<span className="inline-block h-3 w-3 mr-1">ℹ️</span>
|
||||
<span>
|
||||
Use {"{"}content{"}"} to insert the user's input.
|
||||
<span className="ml-2 text-emerald-400">
|
||||
Click to expand editor.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="expected_output"
|
||||
className="text-xs text-neutral-400 mb-1 block"
|
||||
>
|
||||
Expected output (optional)
|
||||
</Label>
|
||||
<Input
|
||||
id="expected_output"
|
||||
placeholder="Ex: JSON report, List of recommendations, etc."
|
||||
value={newTask.expected_output}
|
||||
onChange={(e) =>
|
||||
setNewTask({ ...newTask, expected_output: e.target.value })
|
||||
}
|
||||
className="bg-[#2a2a2a] border-[#444] text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{newTask.enabled_tools && newTask.enabled_tools.length > 0 && (
|
||||
<div className="mt-3">
|
||||
<Label className="text-xs text-neutral-400 mb-1 block">
|
||||
Selected tools:
|
||||
</Label>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{newTask.enabled_tools.map((toolId) => (
|
||||
<Badge
|
||||
key={toolId}
|
||||
className="bg-[#333] text-emerald-400 border border-emerald-400/30"
|
||||
>
|
||||
{toolId}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (newTask.agent_id) setIsToolsModalOpen(true);
|
||||
}}
|
||||
disabled={!newTask.agent_id}
|
||||
className="border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 px-3"
|
||||
>
|
||||
<PenTool className="h-4 w-4 mr-2" />
|
||||
Configure tools
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleAddTask}
|
||||
disabled={!newTask.agent_id || !newTask.description}
|
||||
className="bg-[#222] text-emerald-400 border border-emerald-400 hover:bg-emerald-400/10"
|
||||
>
|
||||
<Save className="h-4 w-4 mr-1" />{" "}
|
||||
{isEditing ? "Update task" : "Add task"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
open={isDescriptionModalOpen}
|
||||
onOpenChange={setIsDescriptionModalOpen}
|
||||
>
|
||||
<DialogContent className="sm:max-w-[1200px] max-h-[90vh] bg-[#1a1a1a] border-[#333] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Task Description</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-hidden flex flex-col min-h-[60vh]">
|
||||
<Textarea
|
||||
value={expandedDescription}
|
||||
onChange={(e) => setExpandedDescription(e.target.value)}
|
||||
className="flex-1 min-h-full bg-[#222] border-[#444] text-white p-4 focus:border-emerald-400 focus:ring-emerald-400 focus:ring-opacity-50 resize-none"
|
||||
placeholder="Enter detailed description for the task..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsDescriptionModalOpen(false)}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveExpandedDescription}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Save description
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={isToolsModalOpen} onOpenChange={setIsToolsModalOpen}>
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[90vh] bg-[#1a1a1a] border-[#333] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">
|
||||
Available tools
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="relative mb-4">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-neutral-400" />
|
||||
<Input
|
||||
placeholder="Search tools..."
|
||||
className="bg-[#222] border-[#444] text-white pl-9"
|
||||
value={toolSearchQuery}
|
||||
onChange={(e) => setToolSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto space-y-2 pr-1">
|
||||
{filteredTools.length > 0 ? (
|
||||
filteredTools.map((tool) => (
|
||||
<div
|
||||
key={tool.id}
|
||||
className="flex items-center space-x-2 p-2 rounded-md hover:bg-[#333] transition duration-150"
|
||||
>
|
||||
<Checkbox
|
||||
id={tool.id}
|
||||
checked={isToolEnabled(tool.id)}
|
||||
onCheckedChange={() => handleToggleTool(tool.id)}
|
||||
className="border-[#444] data-[state=checked]:bg-emerald-400 data-[state=checked]:text-black"
|
||||
/>
|
||||
<Label
|
||||
htmlFor={tool.id}
|
||||
className="cursor-pointer text-white flex-1"
|
||||
>
|
||||
{tool.name}
|
||||
</Label>
|
||||
<Badge className="bg-[#333] text-emerald-400">{tool.id}</Badge>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-neutral-400">No tools available</p>
|
||||
<p className="text-xs text-neutral-500">
|
||||
The tools are obtained from the selected agents in the tasks.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={handleSaveTools}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Save settings
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
184
frontend/app/agents/dialogs/AgentToolDialog.tsx
Normal file
184
frontend/app/agents/dialogs/AgentToolDialog.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/dialogs/AgentToolDialog.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { listAgents } from "@/services/agentService";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
interface AgentToolDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSave: (tool: { id: string; envs: Record<string, string> }) => void;
|
||||
currentAgentId?: string;
|
||||
folderId?: string | null;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
export function AgentToolDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSave,
|
||||
currentAgentId,
|
||||
folderId,
|
||||
clientId,
|
||||
}: AgentToolDialogProps) {
|
||||
const [selectedAgentId, setSelectedAgentId] = useState<string>("");
|
||||
const [search, setSearch] = useState("");
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setSelectedAgentId("");
|
||||
setSearch("");
|
||||
loadAgents();
|
||||
}
|
||||
}, [open, folderId, clientId]);
|
||||
|
||||
const loadAgents = async () => {
|
||||
if (!clientId) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await listAgents(
|
||||
clientId,
|
||||
0,
|
||||
100,
|
||||
folderId || undefined
|
||||
);
|
||||
|
||||
// Filter out the current agent to avoid self-reference
|
||||
const filteredAgents = res.data.filter(agent =>
|
||||
agent.id !== currentAgentId
|
||||
);
|
||||
|
||||
setAgents(filteredAgents);
|
||||
} catch (error) {
|
||||
console.error("Error loading agents:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!selectedAgentId) return;
|
||||
onSave({ id: selectedAgentId, envs: {} });
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const filteredAgents = agents.filter((agent) =>
|
||||
agent.name.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[420px] max-h-[90vh] overflow-hidden flex flex-col bg-[#1a1a1a] border-[#333]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Add Agent Tool</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
Select an agent to add as a tool.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-auto px-2 pb-2 space-y-4">
|
||||
<Input
|
||||
placeholder="Search agent by name..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="mb-2 bg-[#222] border-[#444] text-white placeholder:text-neutral-400"
|
||||
/>
|
||||
<div className="space-y-2 max-h-60 overflow-y-auto pr-1">
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-6">
|
||||
<Loader2 className="h-6 w-6 text-emerald-400 animate-spin" />
|
||||
<div className="mt-2 text-sm text-neutral-400">Loading agents...</div>
|
||||
</div>
|
||||
) : filteredAgents.length === 0 ? (
|
||||
<div className="text-neutral-400 text-sm text-center py-6">
|
||||
{search ? `No agents found matching "${search}"` : "No agents found in this folder."}
|
||||
</div>
|
||||
) : (
|
||||
filteredAgents.map((agent) => (
|
||||
<button
|
||||
key={agent.id}
|
||||
type="button"
|
||||
onClick={() => setSelectedAgentId(agent.id)}
|
||||
className={cn(
|
||||
"w-full flex items-start gap-3 p-3 rounded-md border border-[#333] bg-[#232323] hover:bg-[#222] transition text-left cursor-pointer",
|
||||
selectedAgentId === agent.id && "border-emerald-400 bg-[#1a1a1a] shadow-md"
|
||||
)}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-white text-base">{agent.name}</div>
|
||||
<div className="text-xs text-neutral-400 mt-1">
|
||||
{agent.description || "No description"}
|
||||
</div>
|
||||
<div className="text-[10px] text-neutral-500 mt-1">ID: {agent.id}</div>
|
||||
</div>
|
||||
{selectedAgentId === agent.id && (
|
||||
<span className="ml-2 text-emerald-400 font-bold">Selected</span>
|
||||
)}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="p-4 pt-2 border-t border-[#333]">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
disabled={!selectedAgentId || isLoading}
|
||||
>
|
||||
Add Tool
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
445
frontend/app/agents/dialogs/ApiKeysDialog.tsx
Normal file
445
frontend/app/agents/dialogs/ApiKeysDialog.tsx
Normal file
@@ -0,0 +1,445 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/dialogs/ApiKeysDialog.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { ConfirmationDialog } from "./ConfirmationDialog";
|
||||
import { ApiKey } from "@/services/agentService";
|
||||
import { Edit, Eye, Key, Plus, Trash2, X } from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { availableModelProviders } from "@/types/aiModels";
|
||||
|
||||
interface ApiKeysDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
apiKeys: ApiKey[];
|
||||
isLoading: boolean;
|
||||
onAddApiKey: (apiKey: {
|
||||
name: string;
|
||||
provider: string;
|
||||
key_value: string;
|
||||
}) => Promise<void>;
|
||||
onUpdateApiKey: (
|
||||
id: string,
|
||||
apiKey: {
|
||||
name: string;
|
||||
provider: string;
|
||||
key_value?: string;
|
||||
is_active: boolean;
|
||||
}
|
||||
) => Promise<void>;
|
||||
onDeleteApiKey: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function ApiKeysDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
apiKeys,
|
||||
isLoading,
|
||||
onAddApiKey,
|
||||
onUpdateApiKey,
|
||||
onDeleteApiKey,
|
||||
}: ApiKeysDialogProps) {
|
||||
const [isAddingApiKey, setIsAddingApiKey] = useState(false);
|
||||
const [isEditingApiKey, setIsEditingApiKey] = useState(false);
|
||||
const [currentApiKey, setCurrentApiKey] = useState<
|
||||
Partial<ApiKey & { key_value?: string }>
|
||||
>({});
|
||||
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [apiKeyToDelete, setApiKeyToDelete] = useState<ApiKey | null>(null);
|
||||
const [isApiKeyVisible, setIsApiKeyVisible] = useState(false);
|
||||
|
||||
const handleAddClick = () => {
|
||||
setCurrentApiKey({});
|
||||
setIsAddingApiKey(true);
|
||||
setIsEditingApiKey(false);
|
||||
};
|
||||
|
||||
const handleEditClick = (apiKey: ApiKey) => {
|
||||
setCurrentApiKey({ ...apiKey, key_value: "" });
|
||||
setIsAddingApiKey(true);
|
||||
setIsEditingApiKey(true);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (apiKey: ApiKey) => {
|
||||
setApiKeyToDelete(apiKey);
|
||||
setIsDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveApiKey = async () => {
|
||||
if (
|
||||
!currentApiKey.name ||
|
||||
!currentApiKey.provider ||
|
||||
(!isEditingApiKey && !currentApiKey.key_value)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (currentApiKey.id) {
|
||||
await onUpdateApiKey(currentApiKey.id, {
|
||||
name: currentApiKey.name,
|
||||
provider: currentApiKey.provider,
|
||||
key_value: currentApiKey.key_value,
|
||||
is_active: currentApiKey.is_active !== false,
|
||||
});
|
||||
} else {
|
||||
await onAddApiKey({
|
||||
name: currentApiKey.name,
|
||||
provider: currentApiKey.provider,
|
||||
key_value: currentApiKey.key_value!,
|
||||
});
|
||||
}
|
||||
|
||||
setCurrentApiKey({});
|
||||
setIsAddingApiKey(false);
|
||||
setIsEditingApiKey(false);
|
||||
} catch (error) {
|
||||
console.error("Error saving API key:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!apiKeyToDelete) return;
|
||||
|
||||
try {
|
||||
await onDeleteApiKey(apiKeyToDelete.id);
|
||||
setApiKeyToDelete(null);
|
||||
setIsDeleteDialogOpen(false);
|
||||
} catch (error) {
|
||||
console.error("Error deleting API key:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-hidden flex flex-col bg-[#1a1a1a] border-[#333]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Manage API Keys</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
Add and manage API keys for use in your agents
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-auto p-1">
|
||||
{isAddingApiKey ? (
|
||||
<div className="space-y-4 p-4 bg-[#222] rounded-md">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-medium text-white">
|
||||
{isEditingApiKey ? "Edit Key" : "New Key"}
|
||||
</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsAddingApiKey(false);
|
||||
setIsEditingApiKey(false);
|
||||
setCurrentApiKey({});
|
||||
}}
|
||||
className="text-neutral-400 hover:text-white"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right text-neutral-300">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={currentApiKey.name || ""}
|
||||
onChange={(e) =>
|
||||
setCurrentApiKey({
|
||||
...currentApiKey,
|
||||
name: e.target.value,
|
||||
})
|
||||
}
|
||||
className="col-span-3 bg-[#333] border-[#444] text-white"
|
||||
placeholder="OpenAI GPT-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label
|
||||
htmlFor="provider"
|
||||
className="text-right text-neutral-300"
|
||||
>
|
||||
Provider
|
||||
</Label>
|
||||
<Select
|
||||
value={currentApiKey.provider}
|
||||
onValueChange={(value) =>
|
||||
setCurrentApiKey({
|
||||
...currentApiKey,
|
||||
provider: value,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="col-span-3 bg-[#333] border-[#444] text-white">
|
||||
<SelectValue placeholder="Select Provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#222] border-[#444] text-white">
|
||||
{availableModelProviders.map((provider) => (
|
||||
<SelectItem
|
||||
key={provider.value}
|
||||
value={provider.value}
|
||||
className="data-[selected]:bg-[#333] data-[highlighted]:bg-[#333] !text-white focus:!text-white hover:text-emerald-400 data-[selected]:!text-emerald-400"
|
||||
>
|
||||
{provider.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label
|
||||
htmlFor="key_value"
|
||||
className="text-right text-neutral-300"
|
||||
>
|
||||
Key Value
|
||||
</Label>
|
||||
<div className="col-span-3 relative">
|
||||
<Input
|
||||
id="key_value"
|
||||
value={currentApiKey.key_value || ""}
|
||||
onChange={(e) =>
|
||||
setCurrentApiKey({
|
||||
...currentApiKey,
|
||||
key_value: e.target.value,
|
||||
})
|
||||
}
|
||||
className="bg-[#333] border-[#444] text-white pr-10"
|
||||
type={isApiKeyVisible ? "text" : "password"}
|
||||
placeholder={
|
||||
isEditingApiKey
|
||||
? "Leave blank to keep the current value"
|
||||
: "sk-..."
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
className="absolute right-2 top-1/2 transform -translate-y-1/2 h-7 w-7 p-0 text-neutral-400 hover:text-white"
|
||||
onClick={() => setIsApiKeyVisible(!isApiKeyVisible)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isEditingApiKey && (
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label
|
||||
htmlFor="is_active"
|
||||
className="text-right text-neutral-300"
|
||||
>
|
||||
Status
|
||||
</Label>
|
||||
<div className="col-span-3 flex items-center">
|
||||
<Checkbox
|
||||
id="is_active"
|
||||
checked={currentApiKey.is_active !== false}
|
||||
onCheckedChange={(checked) =>
|
||||
setCurrentApiKey({
|
||||
...currentApiKey,
|
||||
is_active: !!checked,
|
||||
})
|
||||
}
|
||||
className="mr-2 data-[state=checked]:bg-emerald-400 data-[state=checked]:border-emerald-400"
|
||||
/>
|
||||
<Label htmlFor="is_active" className="text-neutral-300">
|
||||
Active
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsAddingApiKey(false);
|
||||
setIsEditingApiKey(false);
|
||||
setCurrentApiKey({});
|
||||
}}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveApiKey}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="animate-spin h-4 w-4 border-2 border-black border-t-transparent rounded-full mr-1"></div>
|
||||
)}
|
||||
{isEditingApiKey ? "Update" : "Add"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium text-white">
|
||||
Available Keys
|
||||
</h3>
|
||||
<Button
|
||||
onClick={handleAddClick}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Key
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-40">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-emerald-400"></div>
|
||||
</div>
|
||||
) : apiKeys.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{apiKeys.map((apiKey) => (
|
||||
<div
|
||||
key={apiKey.id}
|
||||
className="flex items-center justify-between p-3 bg-[#222] rounded-md border border-[#333] hover:border-emerald-400/30"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium text-white">{apiKey.name}</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-[#333] text-emerald-400 border-emerald-400/30"
|
||||
>
|
||||
{apiKey.provider.toUpperCase()}
|
||||
</Badge>
|
||||
<p className="text-xs text-neutral-400">
|
||||
Created on{" "}
|
||||
{new Date(apiKey.created_at).toLocaleDateString()}
|
||||
</p>
|
||||
{!apiKey.is_active && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-[#333] text-red-400 border-red-400/30"
|
||||
>
|
||||
Inactive
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditClick(apiKey)}
|
||||
className="text-neutral-300 hover:text-emerald-400 hover:bg-[#333]"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteClick(apiKey)}
|
||||
className="text-red-500 hover:text-red-400 hover:bg-[#333]"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-10 border border-dashed border-[#333] rounded-md bg-[#222] text-neutral-400">
|
||||
<Key className="mx-auto h-10 w-10 text-neutral-500 mb-3" />
|
||||
<p>You don't have any API keys registered</p>
|
||||
<p className="text-sm mt-1">
|
||||
Add your API keys to use them in your agents
|
||||
</p>
|
||||
<Button
|
||||
onClick={handleAddClick}
|
||||
className="mt-4 bg-[#333] text-emerald-400 hover:bg-[#444]"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Key
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="border-t border-[#333] pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
<ConfirmationDialog
|
||||
open={isDeleteDialogOpen}
|
||||
onOpenChange={setIsDeleteDialogOpen}
|
||||
title="Confirm Delete"
|
||||
description={`Are you sure you want to delete the key "${apiKeyToDelete?.name}"? This action cannot be undone.`}
|
||||
confirmText="Delete"
|
||||
confirmVariant="destructive"
|
||||
onConfirm={handleDeleteConfirm}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
93
frontend/app/agents/dialogs/ConfirmationDialog.tsx
Normal file
93
frontend/app/agents/dialogs/ConfirmationDialog.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/dialogs/ConfirmationDialog.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
interface ConfirmationDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
title: string;
|
||||
description: string;
|
||||
cancelText?: string;
|
||||
confirmText?: string;
|
||||
confirmVariant?: "default" | "destructive";
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
export function ConfirmationDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
title,
|
||||
description,
|
||||
cancelText = "Cancel",
|
||||
confirmText = "Confirm",
|
||||
confirmVariant = "default",
|
||||
onConfirm,
|
||||
}: ConfirmationDialogProps) {
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent className="bg-[#1a1a1a] border-[#333] text-white">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-neutral-400">
|
||||
{description}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white">
|
||||
{cancelText}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onConfirm();
|
||||
}}
|
||||
className={
|
||||
confirmVariant === "destructive"
|
||||
? "bg-red-600 text-white hover:bg-red-700"
|
||||
: "bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
}
|
||||
>
|
||||
{confirmText}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
237
frontend/app/agents/dialogs/CustomMCPDialog.tsx
Normal file
237
frontend/app/agents/dialogs/CustomMCPDialog.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/dialogs/CustomMCPDialog.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { X, Plus } from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface CustomMCPHeader {
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface CustomMCPServer {
|
||||
url: string;
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
|
||||
interface CustomMCPDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSave: (customMCP: CustomMCPServer) => void;
|
||||
initialCustomMCP?: CustomMCPServer | null;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
export function CustomMCPDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSave,
|
||||
initialCustomMCP = null,
|
||||
clientId,
|
||||
}: CustomMCPDialogProps) {
|
||||
const [customMCP, setCustomMCP] = useState<Partial<CustomMCPServer>>({
|
||||
url: "",
|
||||
headers: {},
|
||||
});
|
||||
|
||||
const [headersList, setHeadersList] = useState<CustomMCPHeader[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (initialCustomMCP) {
|
||||
setCustomMCP(initialCustomMCP);
|
||||
const headersList = Object.entries(initialCustomMCP.headers || {}).map(
|
||||
([key, value], index) => ({
|
||||
id: `header-${index}`,
|
||||
key,
|
||||
value,
|
||||
})
|
||||
);
|
||||
setHeadersList(headersList);
|
||||
} else {
|
||||
setCustomMCP({ url: "", headers: {} });
|
||||
setHeadersList([]);
|
||||
}
|
||||
}
|
||||
}, [open, initialCustomMCP]);
|
||||
|
||||
const handleAddHeader = () => {
|
||||
setHeadersList([
|
||||
...headersList,
|
||||
{ id: `header-${Date.now()}`, key: "", value: "" },
|
||||
]);
|
||||
};
|
||||
|
||||
const handleRemoveHeader = (id: string) => {
|
||||
setHeadersList(headersList.filter((header) => header.id !== id));
|
||||
};
|
||||
|
||||
const handleHeaderChange = (
|
||||
id: string,
|
||||
field: "key" | "value",
|
||||
value: string
|
||||
) => {
|
||||
setHeadersList(
|
||||
headersList.map((header) =>
|
||||
header.id === id ? { ...header, [field]: value } : header
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!customMCP.url) return;
|
||||
|
||||
const headersObject: Record<string, string> = {};
|
||||
headersList.forEach((header) => {
|
||||
if (header.key.trim()) {
|
||||
headersObject[header.key] = header.value;
|
||||
}
|
||||
});
|
||||
|
||||
onSave({
|
||||
url: customMCP.url,
|
||||
headers: headersObject,
|
||||
});
|
||||
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-hidden flex flex-col bg-[#1a1a1a] border-[#333]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">
|
||||
Configure Custom MCP
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
Configure the URL and HTTP headers for the custom MCP.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="custom-mcp-url" className="text-neutral-300">
|
||||
MCP URL
|
||||
</Label>
|
||||
<Input
|
||||
id="custom-mcp-url"
|
||||
value={customMCP.url || ""}
|
||||
onChange={(e) =>
|
||||
setCustomMCP({
|
||||
...customMCP,
|
||||
url: e.target.value,
|
||||
})
|
||||
}
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
placeholder="https://meu-servidor-mcp.com/api"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-white">HTTP Headers</h3>
|
||||
<div className="border border-[#444] rounded-md p-3 bg-[#222]">
|
||||
{headersList.map((header) => (
|
||||
<div
|
||||
key={header.id}
|
||||
className="grid grid-cols-5 items-center gap-2 mb-2"
|
||||
>
|
||||
<Input
|
||||
value={header.key}
|
||||
onChange={(e) =>
|
||||
handleHeaderChange(header.id, "key", e.target.value)
|
||||
}
|
||||
className="col-span-2 bg-[#333] border-[#444] text-white"
|
||||
placeholder="Header Name"
|
||||
/>
|
||||
<Input
|
||||
value={header.value}
|
||||
onChange={(e) =>
|
||||
handleHeaderChange(header.id, "value", e.target.value)
|
||||
}
|
||||
className="col-span-2 bg-[#333] border-[#444] text-white"
|
||||
placeholder="Header Value"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveHeader(header.id)}
|
||||
className="col-span-1 h-8 text-red-500 hover:text-red-400 hover:bg-[#444]"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddHeader}
|
||||
className="w-full mt-2 border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add Header
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="p-4 pt-2 border-t border-[#333]">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
disabled={!customMCP.url}
|
||||
>
|
||||
{initialCustomMCP?.url ? "Save Custom MCP" : "Add Custom MCP"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
864
frontend/app/agents/dialogs/CustomToolDialog.tsx
Normal file
864
frontend/app/agents/dialogs/CustomToolDialog.tsx
Normal file
@@ -0,0 +1,864 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/dialogs/CustomToolDialog.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
X,
|
||||
Plus,
|
||||
Info,
|
||||
Trash,
|
||||
Globe,
|
||||
FileJson,
|
||||
LayoutList,
|
||||
Settings,
|
||||
Database,
|
||||
Code,
|
||||
Server,
|
||||
Wand
|
||||
} from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { HTTPTool, HTTPToolParameter } from "@/types/agent";
|
||||
import { sanitizeAgentName } from "@/lib/utils";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
interface CustomToolDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSave: (tool: HTTPTool) => void;
|
||||
initialTool?: HTTPTool | null;
|
||||
}
|
||||
|
||||
export function CustomToolDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSave,
|
||||
initialTool = null,
|
||||
}: CustomToolDialogProps) {
|
||||
const [tool, setTool] = useState<Partial<HTTPTool>>({
|
||||
name: "",
|
||||
method: "GET",
|
||||
endpoint: "",
|
||||
description: "",
|
||||
headers: {},
|
||||
values: {},
|
||||
parameters: {},
|
||||
error_handling: {
|
||||
timeout: 30,
|
||||
retry_count: 0,
|
||||
fallback_response: {},
|
||||
},
|
||||
});
|
||||
|
||||
const [headersList, setHeadersList] = useState<{ id: string; key: string; value: string }[]>([]);
|
||||
const [bodyParams, setBodyParams] = useState<{ id: string; key: string; param: HTTPToolParameter }[]>([]);
|
||||
const [pathParams, setPathParams] = useState<{ id: string; key: string; desc: string }[]>([]);
|
||||
const [queryParams, setQueryParams] = useState<{ id: string; key: string; value: string }[]>([]);
|
||||
const [valuesList, setValuesList] = useState<{ id: string; key: string; value: string }[]>([]);
|
||||
const [timeout, setTimeout] = useState<number>(30);
|
||||
const [fallbackError, setFallbackError] = useState<string>("");
|
||||
const [fallbackMessage, setFallbackMessage] = useState<string>("");
|
||||
const [activeTab, setActiveTab] = useState("basics");
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (initialTool) {
|
||||
setTool(initialTool);
|
||||
setHeadersList(
|
||||
Object.entries(initialTool.headers || {}).map(([key, value], idx) => ({
|
||||
id: `header-${idx}`,
|
||||
key,
|
||||
value,
|
||||
}))
|
||||
);
|
||||
setBodyParams(
|
||||
Object.entries(initialTool.parameters?.body_params || {}).map(([key, param], idx) => ({
|
||||
id: `body-${idx}`,
|
||||
key,
|
||||
param,
|
||||
}))
|
||||
);
|
||||
setPathParams(
|
||||
Object.entries(initialTool.parameters?.path_params || {}).map(([key, desc], idx) => ({
|
||||
id: `path-${idx}`,
|
||||
key,
|
||||
desc: desc as string,
|
||||
}))
|
||||
);
|
||||
setQueryParams(
|
||||
Object.entries(initialTool.parameters?.query_params || {}).map(([key, value], idx) => ({
|
||||
id: `query-${idx}`,
|
||||
key,
|
||||
value: value as string,
|
||||
}))
|
||||
);
|
||||
setValuesList(
|
||||
Object.entries(initialTool.values || {}).map(([key, value], idx) => ({
|
||||
id: `val-${idx}`,
|
||||
key,
|
||||
value: value as string,
|
||||
}))
|
||||
);
|
||||
setTimeout(initialTool.error_handling?.timeout || 30);
|
||||
setFallbackError(initialTool.error_handling?.fallback_response?.error || "");
|
||||
setFallbackMessage(initialTool.error_handling?.fallback_response?.message || "");
|
||||
} else {
|
||||
setTool({
|
||||
name: "",
|
||||
method: "GET",
|
||||
endpoint: "",
|
||||
description: "",
|
||||
headers: {},
|
||||
values: {},
|
||||
parameters: {},
|
||||
error_handling: {
|
||||
timeout: 30,
|
||||
retry_count: 0,
|
||||
fallback_response: {},
|
||||
},
|
||||
});
|
||||
setHeadersList([]);
|
||||
setBodyParams([]);
|
||||
setPathParams([]);
|
||||
setQueryParams([]);
|
||||
setValuesList([]);
|
||||
setTimeout(30);
|
||||
setFallbackError("");
|
||||
setFallbackMessage("");
|
||||
}
|
||||
setActiveTab("basics");
|
||||
}
|
||||
}, [open, initialTool]);
|
||||
|
||||
const handleAddHeader = () => {
|
||||
setHeadersList([...headersList, { id: `header-${Date.now()}`, key: "", value: "" }]);
|
||||
};
|
||||
const handleRemoveHeader = (id: string) => {
|
||||
setHeadersList(headersList.filter((h) => h.id !== id));
|
||||
};
|
||||
const handleHeaderChange = (id: string, field: "key" | "value", value: string) => {
|
||||
setHeadersList(headersList.map((h) => (h.id === id ? { ...h, [field]: value } : h)));
|
||||
};
|
||||
|
||||
const handleAddBodyParam = () => {
|
||||
setBodyParams([
|
||||
...bodyParams,
|
||||
{
|
||||
id: `body-${Date.now()}`,
|
||||
key: "",
|
||||
param: { type: "string", required: false, description: "" },
|
||||
},
|
||||
]);
|
||||
};
|
||||
const handleRemoveBodyParam = (id: string) => {
|
||||
setBodyParams(bodyParams.filter((p) => p.id !== id));
|
||||
};
|
||||
const handleBodyParamChange = (id: string, field: "key" | keyof HTTPToolParameter, value: string | boolean) => {
|
||||
setBodyParams(
|
||||
bodyParams.map((p) =>
|
||||
p.id === id
|
||||
? field === "key"
|
||||
? { ...p, key: value as string }
|
||||
: { ...p, param: { ...p.param, [field]: value } }
|
||||
: p
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// Path Params
|
||||
const handleAddPathParam = () => {
|
||||
setPathParams([...pathParams, { id: `path-${Date.now()}`, key: "", desc: "" }]);
|
||||
};
|
||||
const handleRemovePathParam = (id: string) => {
|
||||
setPathParams(pathParams.filter((p) => p.id !== id));
|
||||
};
|
||||
const handlePathParamChange = (id: string, field: "key" | "desc", value: string) => {
|
||||
setPathParams(pathParams.map((p) => (p.id === id ? { ...p, [field]: value } : p)));
|
||||
};
|
||||
|
||||
// Query Params
|
||||
const handleAddQueryParam = () => {
|
||||
setQueryParams([...queryParams, { id: `query-${Date.now()}`, key: "", value: "" }]);
|
||||
};
|
||||
const handleRemoveQueryParam = (id: string) => {
|
||||
setQueryParams(queryParams.filter((q) => q.id !== id));
|
||||
};
|
||||
const handleQueryParamChange = (id: string, field: "key" | "value", value: string) => {
|
||||
setQueryParams(queryParams.map((q) => (q.id === id ? { ...q, [field]: value } : q)));
|
||||
};
|
||||
|
||||
// Values
|
||||
const handleAddValue = () => {
|
||||
setValuesList([...valuesList, { id: `val-${Date.now()}`, key: "", value: "" }]);
|
||||
};
|
||||
const handleRemoveValue = (id: string) => {
|
||||
setValuesList(valuesList.filter((v) => v.id !== id));
|
||||
};
|
||||
const handleValueChange = (id: string, field: "key" | "value", value: string) => {
|
||||
setValuesList(valuesList.map((v) => (v.id === id ? { ...v, [field]: value } : v)));
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!tool.name || !tool.endpoint) return;
|
||||
const headersObject: Record<string, string> = {};
|
||||
headersList.forEach((h) => {
|
||||
if (h.key.trim()) headersObject[h.key] = h.value;
|
||||
});
|
||||
const bodyParamsObject: Record<string, HTTPToolParameter> = {};
|
||||
bodyParams.forEach((p) => {
|
||||
if (p.key.trim()) bodyParamsObject[p.key] = p.param;
|
||||
});
|
||||
const pathParamsObject: Record<string, string> = {};
|
||||
pathParams.forEach((p) => {
|
||||
if (p.key.trim()) pathParamsObject[p.key] = p.desc;
|
||||
});
|
||||
const queryParamsObject: Record<string, string> = {};
|
||||
queryParams.forEach((q) => {
|
||||
if (q.key.trim()) queryParamsObject[q.key] = q.value;
|
||||
});
|
||||
const valuesObject: Record<string, string> = {};
|
||||
valuesList.forEach((v) => {
|
||||
if (v.key.trim()) valuesObject[v.key] = v.value;
|
||||
});
|
||||
|
||||
// Sanitize the tool name
|
||||
const sanitizedName = sanitizeAgentName(tool.name);
|
||||
|
||||
onSave({
|
||||
...(tool as HTTPTool),
|
||||
name: sanitizedName,
|
||||
headers: headersObject,
|
||||
values: valuesObject,
|
||||
parameters: {
|
||||
...tool.parameters,
|
||||
body_params: bodyParamsObject,
|
||||
path_params: pathParamsObject,
|
||||
query_params: queryParamsObject,
|
||||
},
|
||||
error_handling: {
|
||||
timeout,
|
||||
retry_count: tool.error_handling?.retry_count ?? 0,
|
||||
fallback_response: {
|
||||
error: fallbackError,
|
||||
message: fallbackMessage,
|
||||
},
|
||||
},
|
||||
} as HTTPTool);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const ParamField = ({
|
||||
children,
|
||||
label,
|
||||
tooltip
|
||||
}: {
|
||||
children: React.ReactNode,
|
||||
label: string,
|
||||
tooltip?: string
|
||||
}) => (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label className="text-sm font-medium text-neutral-200">
|
||||
{label}
|
||||
</Label>
|
||||
{tooltip && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-3.5 w-3.5 text-neutral-400 cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-neutral-800 border-neutral-700 text-white p-3 max-w-sm">
|
||||
<p className="text-xs">{tooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const FieldList = <T extends Record<string, any>>({
|
||||
items,
|
||||
onAdd,
|
||||
onRemove,
|
||||
onChange,
|
||||
fields,
|
||||
addText,
|
||||
emptyText,
|
||||
icon
|
||||
}: {
|
||||
items: T[],
|
||||
onAdd: () => void,
|
||||
onRemove: (id: string) => void,
|
||||
onChange: (id: string, field: string, value: any) => void,
|
||||
fields: { name: string, field: string, placeholder: string, width: number, type?: string }[],
|
||||
addText: string,
|
||||
emptyText: string,
|
||||
icon: React.ReactNode
|
||||
}) => (
|
||||
<div className="border border-neutral-700 rounded-md p-3 bg-neutral-800/50">
|
||||
{items.length > 0 ? (
|
||||
<div className="space-y-2 mb-3">
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="flex items-center gap-2 group">
|
||||
<div className="flex-1 flex gap-2 w-full">
|
||||
{fields.map(field => {
|
||||
// Calculate percentage width based on the field's width value
|
||||
const widthPercent = (field.width / 12) * 100;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={field.name}
|
||||
className="flex-shrink-0"
|
||||
style={{ width: `${widthPercent}%` }}
|
||||
>
|
||||
{field.type === 'select' ? (
|
||||
<select
|
||||
value={(field.field.includes('.')
|
||||
? item.param?.[field.field.split('.')[1]]
|
||||
: item[field.field]) || ''}
|
||||
onChange={(e) => onChange(
|
||||
item.id,
|
||||
field.field,
|
||||
e.target.value
|
||||
)}
|
||||
className="w-full h-9 px-3 py-1 rounded-md bg-neutral-900 border border-neutral-700 text-white text-sm"
|
||||
>
|
||||
<option value="string">string</option>
|
||||
<option value="number">number</option>
|
||||
<option value="boolean">boolean</option>
|
||||
</select>
|
||||
) : field.type === 'checkbox' ? (
|
||||
<div className="flex items-center h-9 w-full justify-center">
|
||||
<label className="flex items-center gap-1.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.param?.required || false}
|
||||
onChange={(e) => onChange(
|
||||
item.id,
|
||||
field.field,
|
||||
(e.target as HTMLInputElement).checked
|
||||
)}
|
||||
className="accent-emerald-400 rounded"
|
||||
/>
|
||||
<span className="text-xs text-neutral-300">Required</span>
|
||||
</label>
|
||||
</div>
|
||||
) : (
|
||||
<Input
|
||||
value={(field.field.includes('.')
|
||||
? item.param?.[field.field.split('.')[1]]
|
||||
: item[field.field]) || ''}
|
||||
onChange={(e) => onChange(
|
||||
item.id,
|
||||
field.field,
|
||||
e.target.value
|
||||
)}
|
||||
className="h-9 w-full bg-neutral-900 border-neutral-700 text-white placeholder:text-neutral-500 text-sm"
|
||||
placeholder={field.placeholder}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onRemove(item.id)}
|
||||
className="h-9 w-9 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity text-red-400 hover:text-red-300 hover:bg-red-900/20"
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-6 flex flex-col items-center justify-center text-center">
|
||||
{icon}
|
||||
<p className="mt-2 text-neutral-400 text-sm">{emptyText}</p>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onAdd}
|
||||
className="w-full border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/10 bg-neutral-800/30"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1.5" /> {addText}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[850px] max-h-[90vh] overflow-hidden flex flex-col bg-neutral-900 border-neutral-700 p-0">
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Header Area */}
|
||||
<div className="flex items-start justify-between p-6 border-b border-neutral-800">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white flex items-center gap-2">
|
||||
<Wand className="h-5 w-5 text-emerald-400" />
|
||||
{initialTool ? "Edit Custom Tool" : "Create Custom Tool"}
|
||||
</h2>
|
||||
<p className="text-neutral-400 text-sm mt-1">
|
||||
Configure an HTTP tool for your agent to interact with external APIs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="outline" className="bg-neutral-800 text-emerald-400 border-emerald-500/30 uppercase text-xs font-semibold px-2 py-0.5">
|
||||
{tool.method || "GET"}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Left Side - Navigation */}
|
||||
<div className="w-[200px] bg-neutral-800/50 border-r border-neutral-800 flex-shrink-0">
|
||||
<div className="py-4">
|
||||
<nav className="space-y-1 px-2">
|
||||
{[
|
||||
{ id: 'basics', label: 'Basic Info', icon: <Info className="h-4 w-4" /> },
|
||||
{ id: 'endpoint', label: 'Endpoint', icon: <Globe className="h-4 w-4" /> },
|
||||
{ id: 'headers', label: 'Headers', icon: <Server className="h-4 w-4" /> },
|
||||
{ id: 'body', label: 'Body Params', icon: <FileJson className="h-4 w-4" /> },
|
||||
{ id: 'path', label: 'Path Params', icon: <Code className="h-4 w-4" /> },
|
||||
{ id: 'query', label: 'Query Params', icon: <LayoutList className="h-4 w-4" /> },
|
||||
{ id: 'defaults', label: 'Default Values', icon: <Database className="h-4 w-4" /> },
|
||||
{ id: 'error', label: 'Error Handling', icon: <Settings className="h-4 w-4" /> },
|
||||
].map(item => (
|
||||
<button
|
||||
key={item.id}
|
||||
className={cn(
|
||||
"w-full flex items-center gap-2 px-3 py-2 text-sm rounded-md transition-colors",
|
||||
activeTab === item.id
|
||||
? "bg-emerald-500/10 text-emerald-400"
|
||||
: "text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800"
|
||||
)}
|
||||
onClick={() => setActiveTab(item.id)}
|
||||
>
|
||||
{item.icon}
|
||||
<span>{item.label}</span>
|
||||
{(
|
||||
(item.id === 'headers' && headersList.length > 0) ||
|
||||
(item.id === 'body' && bodyParams.length > 0) ||
|
||||
(item.id === 'path' && pathParams.length > 0) ||
|
||||
(item.id === 'query' && queryParams.length > 0) ||
|
||||
(item.id === 'defaults' && valuesList.length > 0)
|
||||
) && (
|
||||
<span className="ml-auto bg-emerald-500/20 text-emerald-400 text-xs rounded-full px-1.5 py-0.5 min-w-[18px]">
|
||||
{item.id === 'headers' && headersList.length}
|
||||
{item.id === 'body' && bodyParams.length}
|
||||
{item.id === 'path' && pathParams.length}
|
||||
{item.id === 'query' && queryParams.length}
|
||||
{item.id === 'defaults' && valuesList.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side - Content */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{activeTab === 'basics' && (
|
||||
<div className="p-6">
|
||||
<div className="space-y-6">
|
||||
<ParamField
|
||||
label="Tool Name"
|
||||
tooltip="A unique identifier for this tool. Will be used by the agent to reference this tool."
|
||||
>
|
||||
<Input
|
||||
value={tool.name || ""}
|
||||
onChange={(e) => setTool({ ...tool, name: e.target.value })}
|
||||
onBlur={(e) => {
|
||||
const sanitizedName = sanitizeAgentName(e.target.value);
|
||||
if (sanitizedName !== e.target.value) {
|
||||
setTool({ ...tool, name: sanitizedName });
|
||||
}
|
||||
}}
|
||||
className="bg-neutral-800 border-neutral-700 text-white"
|
||||
placeholder="e.g. weatherApi, searchTool"
|
||||
/>
|
||||
</ParamField>
|
||||
|
||||
<ParamField
|
||||
label="Description"
|
||||
tooltip="A clear description of what this tool does. The agent will use this to determine when to use the tool."
|
||||
>
|
||||
<Input
|
||||
value={tool.description || ""}
|
||||
onChange={(e) => setTool({ ...tool, description: e.target.value })}
|
||||
className="bg-neutral-800 border-neutral-700 text-white"
|
||||
placeholder="Provides weather information for a given location"
|
||||
/>
|
||||
</ParamField>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'endpoint' && (
|
||||
<div className="p-6">
|
||||
<div className="space-y-6">
|
||||
<ParamField
|
||||
label="HTTP Method"
|
||||
tooltip="The HTTP method to use for the request."
|
||||
>
|
||||
<div className="flex gap-1">
|
||||
{['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].map(method => (
|
||||
<Button
|
||||
key={method}
|
||||
type="button"
|
||||
onClick={() => setTool({ ...tool, method })}
|
||||
className={cn(
|
||||
"px-4 py-2 rounded font-medium text-sm flex-1",
|
||||
tool.method === method
|
||||
? "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30"
|
||||
: "bg-neutral-800 text-neutral-400 border border-neutral-700 hover:bg-neutral-700 hover:text-neutral-200"
|
||||
)}
|
||||
>
|
||||
{method}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</ParamField>
|
||||
|
||||
<ParamField
|
||||
label="Endpoint URL"
|
||||
tooltip="The complete URL to call. Can include path parameters with the format {paramName}."
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
value={tool.endpoint || ""}
|
||||
onChange={(e) => setTool({ ...tool, endpoint: e.target.value })}
|
||||
className="bg-neutral-800 border-neutral-700 text-white font-mono"
|
||||
placeholder="https://api.example.com/v1/resource/{id}"
|
||||
/>
|
||||
{tool.endpoint && tool.endpoint.includes('{') && (
|
||||
<p className="text-xs text-amber-400">
|
||||
<Info className="h-3 w-3 inline-block mr-1" />
|
||||
URL contains path variables. Don't forget to define them in the Path Parameters section.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</ParamField>
|
||||
|
||||
<div className="p-4 bg-neutral-800/80 border border-emerald-600/20 rounded-md mt-6">
|
||||
<h3 className="text-emerald-400 text-sm font-medium mb-2 flex items-center">
|
||||
<Info className="h-4 w-4 mr-2" /> How to use variables in your endpoint
|
||||
</h3>
|
||||
<p className="text-neutral-300 text-sm mb-3">
|
||||
You can use dynamic variables in your endpoint using the <code className="bg-neutral-700 px-1.5 py-0.5 rounded text-emerald-300">{"{VARIABLE_NAME}"}</code> syntax.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 gap-4 text-xs">
|
||||
<div>
|
||||
<h4 className="font-medium text-white mb-1">Example:</h4>
|
||||
<code className="block bg-neutral-900 text-neutral-200 p-2 rounded">
|
||||
https://api.example.com/users/<span className="text-emerald-400">{"{userId}"}</span>/profile
|
||||
</code>
|
||||
<p className="mt-1 text-neutral-400">Define <code className="bg-neutral-700 px-1 py-0.5 rounded text-emerald-300">userId</code> as a Path Parameter</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'headers' && (
|
||||
<div className="p-6">
|
||||
<ParamField
|
||||
label="HTTP Headers"
|
||||
tooltip="Headers to send with each request. Common examples include Authorization, Content-Type, Accept, etc."
|
||||
>
|
||||
<FieldList
|
||||
items={headersList}
|
||||
onAdd={handleAddHeader}
|
||||
onRemove={handleRemoveHeader}
|
||||
onChange={(id, field, value) => {
|
||||
handleHeaderChange(
|
||||
id,
|
||||
field as "key" | "value",
|
||||
value as string
|
||||
);
|
||||
}}
|
||||
fields={[
|
||||
{ name: "Header Name", field: "key", placeholder: "e.g. Authorization", width: 6 },
|
||||
{ name: "Header Value", field: "value", placeholder: "e.g. Bearer token123", width: 6 }
|
||||
]}
|
||||
addText="Add Header"
|
||||
emptyText="No headers configured. Add headers to customize your HTTP requests."
|
||||
icon={<Server className="h-10 w-10 text-neutral-700" />}
|
||||
/>
|
||||
</ParamField>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'body' && (
|
||||
<div className="p-6">
|
||||
<div className="mb-6 p-4 bg-neutral-800/80 border border-emerald-600/20 rounded-md">
|
||||
<h3 className="text-emerald-400 text-sm font-medium mb-2 flex items-center">
|
||||
<Info className="h-4 w-4 mr-2" /> About Body Parameters
|
||||
</h3>
|
||||
<p className="text-neutral-300 text-sm">
|
||||
Parameters that will be sent in the request body. Only applicable for POST, PUT, and PATCH methods.
|
||||
You can use variables like <code className="bg-neutral-700 px-1.5 py-0.5 rounded text-emerald-300">{"{variableName}"}</code> that will be replaced at runtime.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FieldList
|
||||
items={bodyParams}
|
||||
onAdd={handleAddBodyParam}
|
||||
onRemove={handleRemoveBodyParam}
|
||||
onChange={(id, field, value) => {
|
||||
if (field === "key") {
|
||||
handleBodyParamChange(id, "key", value as string);
|
||||
} else if (field.startsWith("param.")) {
|
||||
const paramField = field.split('.')[1] as keyof HTTPToolParameter;
|
||||
handleBodyParamChange(id, paramField,
|
||||
paramField === "required" ? value as boolean : value as string
|
||||
);
|
||||
}
|
||||
}}
|
||||
fields={[
|
||||
{ name: "Parameter Name", field: "key", placeholder: "e.g. userId", width: 4 },
|
||||
{ name: "Type", field: "param.type", placeholder: "", width: 2, type: "select" },
|
||||
{ name: "Description", field: "param.description", placeholder: "What this parameter does", width: 4 },
|
||||
{ name: "Required", field: "param.required", placeholder: "", width: 2, type: "checkbox" }
|
||||
]}
|
||||
addText="Add Body Parameter"
|
||||
emptyText="No body parameters configured. Add parameters for POST/PUT/PATCH requests."
|
||||
icon={<FileJson className="h-10 w-10 text-neutral-700" />}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'path' && (
|
||||
<div className="p-6">
|
||||
<div className="mb-6 p-4 bg-neutral-800/80 border border-emerald-600/20 rounded-md">
|
||||
<h3 className="text-emerald-400 text-sm font-medium mb-2 flex items-center">
|
||||
<Info className="h-4 w-4 mr-2" /> About Path Parameters
|
||||
</h3>
|
||||
<p className="text-neutral-300 text-sm">
|
||||
Path parameters are placeholders in your URL path that will be replaced with actual values.
|
||||
For example, in <code className="bg-neutral-700 px-1.5 py-0.5 rounded text-emerald-300">https://api.example.com/users/{"{userId}"}</code>,
|
||||
<code className="bg-neutral-700 px-1 py-0.5 rounded text-emerald-300">userId</code> is a path parameter.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FieldList
|
||||
items={pathParams}
|
||||
onAdd={handleAddPathParam}
|
||||
onRemove={handleRemovePathParam}
|
||||
onChange={(id, field, value) => {
|
||||
handlePathParamChange(
|
||||
id,
|
||||
field as "key" | "desc",
|
||||
value as string
|
||||
);
|
||||
}}
|
||||
fields={[
|
||||
{ name: "Parameter Name", field: "key", placeholder: "e.g. userId", width: 4 },
|
||||
{ name: "Description", field: "desc", placeholder: "What this parameter represents", width: 6 },
|
||||
{ name: "Required", field: "required", placeholder: "", width: 2, type: "checkbox" }
|
||||
]}
|
||||
addText="Add Path Parameter"
|
||||
emptyText="No path parameters configured. Add parameters if your endpoint URL contains {placeholders}."
|
||||
icon={<Code className="h-10 w-10 text-neutral-700" />}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'query' && (
|
||||
<div className="p-6">
|
||||
<div className="mb-6 p-4 bg-neutral-800/80 border border-emerald-600/20 rounded-md">
|
||||
<h3 className="text-emerald-400 text-sm font-medium mb-2 flex items-center">
|
||||
<Info className="h-4 w-4 mr-2" /> About Query Parameters
|
||||
</h3>
|
||||
<p className="text-neutral-300 text-sm">
|
||||
Query parameters are added to the URL after a question mark (?) to filter or customize the request.
|
||||
For example: <code className="bg-neutral-700 px-1.5 py-0.5 rounded text-emerald-300">https://api.example.com/search?query=term&limit=10</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FieldList
|
||||
items={queryParams}
|
||||
onAdd={handleAddQueryParam}
|
||||
onRemove={handleRemoveQueryParam}
|
||||
onChange={(id, field, value) => {
|
||||
handleQueryParamChange(
|
||||
id,
|
||||
field as "key" | "value",
|
||||
value as string
|
||||
);
|
||||
}}
|
||||
fields={[
|
||||
{ name: "Parameter Name", field: "key", placeholder: "e.g. search", width: 4 },
|
||||
{ name: "Description", field: "value", placeholder: "What this parameter does", width: 6 },
|
||||
{ name: "Required", field: "required", placeholder: "", width: 2, type: "checkbox" }
|
||||
]}
|
||||
addText="Add Query Parameter"
|
||||
emptyText="No query parameters configured. Add parameters to customize your URL query string."
|
||||
icon={<LayoutList className="h-10 w-10 text-neutral-700" />}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'defaults' && (
|
||||
<div className="p-6">
|
||||
<div className="mb-6 p-4 bg-neutral-800/80 border border-emerald-600/20 rounded-md">
|
||||
<h3 className="text-emerald-400 text-sm font-medium mb-2 flex items-center">
|
||||
<Info className="h-4 w-4 mr-2" /> About Default Values
|
||||
</h3>
|
||||
<p className="text-neutral-300 text-sm">
|
||||
Set default values for parameters you've defined in the Body, Path, or Query sections.
|
||||
These values will be used when the parameter isn't explicitly provided during a tool call.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FieldList
|
||||
items={valuesList}
|
||||
onAdd={handleAddValue}
|
||||
onRemove={handleRemoveValue}
|
||||
onChange={(id, field, value) => {
|
||||
handleValueChange(
|
||||
id,
|
||||
field as "key" | "value",
|
||||
value as string
|
||||
);
|
||||
}}
|
||||
fields={[
|
||||
{ name: "Parameter Name", field: "key", placeholder: "Name of an existing parameter", width: 5 },
|
||||
{ name: "Default Value", field: "value", placeholder: "Value to use if not provided", width: 7 }
|
||||
]}
|
||||
addText="Add Default Value"
|
||||
emptyText="No default values configured. Add default values for your previously defined parameters."
|
||||
icon={<Database className="h-10 w-10 text-neutral-700" />}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'error' && (
|
||||
<div className="p-6">
|
||||
<div className="mb-6 p-4 bg-neutral-800/80 border border-emerald-600/20 rounded-md">
|
||||
<h3 className="text-emerald-400 text-sm font-medium mb-2 flex items-center">
|
||||
<Info className="h-4 w-4 mr-2" /> About Error Handling
|
||||
</h3>
|
||||
<p className="text-neutral-300 text-sm">
|
||||
Configure how the tool should handle errors and timeouts when making HTTP requests.
|
||||
Proper error handling ensures reliable operation of your agent.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<ParamField
|
||||
label="Timeout (seconds)"
|
||||
tooltip="Maximum time to wait for a response before considering the request failed."
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={timeout}
|
||||
onChange={(e) => setTimeout(Number(e.target.value))}
|
||||
className="w-32 bg-neutral-800 border-neutral-700 text-white"
|
||||
/>
|
||||
</ParamField>
|
||||
|
||||
<ParamField
|
||||
label="Fallback Error Code"
|
||||
tooltip="Error code to return if the request fails."
|
||||
>
|
||||
<Input
|
||||
value={fallbackError}
|
||||
onChange={(e) => setFallbackError(e.target.value)}
|
||||
className="bg-neutral-800 border-neutral-700 text-white"
|
||||
placeholder="e.g. API_ERROR"
|
||||
/>
|
||||
</ParamField>
|
||||
|
||||
<ParamField
|
||||
label="Fallback Error Message"
|
||||
tooltip="Human-readable message to return if the request fails."
|
||||
>
|
||||
<Input
|
||||
value={fallbackMessage}
|
||||
onChange={(e) => setFallbackMessage(e.target.value)}
|
||||
className="bg-neutral-800 border-neutral-700 text-white"
|
||||
placeholder="e.g. Failed to retrieve data from the API."
|
||||
/>
|
||||
</ParamField>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Area */}
|
||||
<div className="border-t border-neutral-800 p-4 flex items-center justify-between">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="text-neutral-400 hover:text-neutral-100 hover:bg-neutral-800"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="bg-emerald-500 text-neutral-950 hover:bg-emerald-400"
|
||||
disabled={!tool.name || !tool.endpoint}
|
||||
>
|
||||
{initialTool ? "Save Changes" : "Create Tool"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
158
frontend/app/agents/dialogs/FolderDialog.tsx
Normal file
158
frontend/app/agents/dialogs/FolderDialog.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/dialogs/FolderDialog.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface Folder {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface FolderDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSave: (folder: { name: string; description: string }) => Promise<void>;
|
||||
editingFolder: Folder | null;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function FolderDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSave,
|
||||
editingFolder,
|
||||
isLoading = false,
|
||||
}: FolderDialogProps) {
|
||||
const [folder, setFolder] = useState<{
|
||||
name: string;
|
||||
description: string;
|
||||
}>({
|
||||
name: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (editingFolder) {
|
||||
setFolder({
|
||||
name: editingFolder.name,
|
||||
description: editingFolder.description,
|
||||
});
|
||||
} else {
|
||||
setFolder({
|
||||
name: "",
|
||||
description: "",
|
||||
});
|
||||
}
|
||||
}, [editingFolder, open]);
|
||||
|
||||
const handleSave = async () => {
|
||||
await onSave(folder);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[500px] bg-[#1a1a1a] border-[#333] text-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingFolder ? "Edit Folder" : "New Folder"}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
{editingFolder
|
||||
? "Update the existing folder information"
|
||||
: "Fill in the information to create a new folder"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="folder-name" className="text-neutral-300">
|
||||
Folder Name
|
||||
</Label>
|
||||
<Input
|
||||
id="folder-name"
|
||||
value={folder.name}
|
||||
onChange={(e) =>
|
||||
setFolder({ ...folder, name: e.target.value })
|
||||
}
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="folder-description" className="text-neutral-300">
|
||||
Description (optional)
|
||||
</Label>
|
||||
<Textarea
|
||||
id="folder-description"
|
||||
value={folder.description}
|
||||
onChange={(e) =>
|
||||
setFolder({ ...folder, description: e.target.value })
|
||||
}
|
||||
className="bg-[#222] border-[#444] text-white resize-none h-20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
disabled={!folder.name || isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="animate-spin h-4 w-4 border-2 border-black border-t-transparent rounded-full mr-1"></div>
|
||||
) : null}
|
||||
{editingFolder ? "Save Changes" : "Create Folder"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
239
frontend/app/agents/dialogs/ImportAgentDialog.tsx
Normal file
239
frontend/app/agents/dialogs/ImportAgentDialog.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/dialogs/ImportAgentDialog.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 15, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { useState, useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Loader2, Upload, FileJson } from "lucide-react";
|
||||
import { importAgentFromJson } from "@/services/agentService";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
interface ImportAgentDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSuccess: () => void;
|
||||
clientId: string;
|
||||
folderId?: string | null;
|
||||
}
|
||||
|
||||
export function ImportAgentDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSuccess,
|
||||
clientId,
|
||||
folderId,
|
||||
}: ImportAgentDialogProps) {
|
||||
const { toast } = useToast();
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const selectedFile = e.target.files?.[0];
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragActive(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragActive(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragActive(false);
|
||||
|
||||
const droppedFile = e.dataTransfer.files?.[0];
|
||||
if (droppedFile && droppedFile.type === "application/json") {
|
||||
setFile(droppedFile);
|
||||
} else {
|
||||
toast({
|
||||
title: "Invalid file",
|
||||
description: "Please upload a JSON file",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectFile = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!file || !clientId) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
await importAgentFromJson(file, clientId, folderId);
|
||||
|
||||
toast({
|
||||
title: "Import successful",
|
||||
description: folderId
|
||||
? "Agent was imported successfully and added to the current folder"
|
||||
: "Agent was imported successfully",
|
||||
});
|
||||
|
||||
// Call the success callback to refresh the agent list
|
||||
onSuccess();
|
||||
|
||||
// Close the dialog
|
||||
onOpenChange(false);
|
||||
|
||||
// Reset state
|
||||
setFile(null);
|
||||
} catch (error) {
|
||||
console.error("Error importing agent:", error);
|
||||
toast({
|
||||
title: "Import failed",
|
||||
description: "There was an error importing the agent",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
if (!isLoading) {
|
||||
setFile(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(newOpen) => {
|
||||
if (!newOpen) {
|
||||
resetState();
|
||||
}
|
||||
onOpenChange(newOpen);
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-[500px] bg-[#1a1a1a] border-[#333] text-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Import Agent</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
Upload a JSON file to import an agent
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col space-y-4 py-4">
|
||||
<div
|
||||
className={`h-40 border-2 border-dashed rounded-md flex flex-col items-center justify-center p-4 transition-colors cursor-pointer ${
|
||||
dragActive
|
||||
? "border-emerald-400 bg-emerald-900/20"
|
||||
: file
|
||||
? "border-emerald-600 bg-emerald-900/10"
|
||||
: "border-[#444] hover:border-emerald-600/50"
|
||||
}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={handleSelectFile}
|
||||
>
|
||||
{file ? (
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<FileJson className="h-10 w-10 text-emerald-400" />
|
||||
<span className="text-emerald-400 font-medium">{file.name}</span>
|
||||
<span className="text-neutral-400 text-xs">
|
||||
{Math.round(file.size / 1024)} KB
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<Upload className="h-10 w-10 text-neutral-500" />
|
||||
<p className="text-neutral-400 text-center">
|
||||
Drag & drop your JSON file here or click to browse
|
||||
</p>
|
||||
<span className="text-neutral-500 text-xs">
|
||||
Only JSON files are accepted
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
accept=".json,application/json"
|
||||
style={{ display: "none" }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
disabled={isLoading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
disabled={!file || isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Importing...
|
||||
</>
|
||||
) : (
|
||||
"Import Agent"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
278
frontend/app/agents/dialogs/MCPDialog.tsx
Normal file
278
frontend/app/agents/dialogs/MCPDialog.tsx
Normal file
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/dialogs/MCPDialog.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { MCPServer } from "@/types/mcpServer";
|
||||
import { Server } from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface MCPDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSave: (mcpConfig: {
|
||||
id: string;
|
||||
envs: Record<string, string>;
|
||||
tools: string[];
|
||||
}) => void;
|
||||
availableMCPs: MCPServer[];
|
||||
selectedMCP?: MCPServer | null;
|
||||
initialEnvs?: Record<string, string>;
|
||||
initialTools?: string[];
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
export function MCPDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSave,
|
||||
availableMCPs,
|
||||
selectedMCP: initialSelectedMCP = null,
|
||||
initialEnvs = {},
|
||||
initialTools = [],
|
||||
clientId,
|
||||
}: MCPDialogProps) {
|
||||
const [selectedMCP, setSelectedMCP] = useState<MCPServer | null>(null);
|
||||
const [mcpEnvs, setMcpEnvs] = useState<Record<string, string>>({});
|
||||
const [selectedMCPTools, setSelectedMCPTools] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (initialSelectedMCP) {
|
||||
setSelectedMCP(initialSelectedMCP);
|
||||
setMcpEnvs(initialEnvs);
|
||||
setSelectedMCPTools(initialTools);
|
||||
} else {
|
||||
setSelectedMCP(null);
|
||||
setMcpEnvs({});
|
||||
setSelectedMCPTools([]);
|
||||
}
|
||||
}
|
||||
}, [open, initialSelectedMCP, initialEnvs, initialTools]);
|
||||
|
||||
const handleSelectMCP = (value: string) => {
|
||||
const mcp = availableMCPs.find((m) => m.id === value);
|
||||
if (mcp) {
|
||||
setSelectedMCP(mcp);
|
||||
const initialEnvs: Record<string, string> = {};
|
||||
Object.keys(mcp.environments || {}).forEach((key) => {
|
||||
initialEnvs[key] = "";
|
||||
});
|
||||
setMcpEnvs(initialEnvs);
|
||||
setSelectedMCPTools([]);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMCPTool = (tool: string) => {
|
||||
if (selectedMCPTools.includes(tool)) {
|
||||
setSelectedMCPTools(selectedMCPTools.filter((t) => t !== tool));
|
||||
} else {
|
||||
setSelectedMCPTools([...selectedMCPTools, tool]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!selectedMCP) return;
|
||||
|
||||
onSave({
|
||||
id: selectedMCP.id,
|
||||
envs: mcpEnvs,
|
||||
tools: selectedMCPTools,
|
||||
});
|
||||
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-hidden flex flex-col bg-[#1a1a1a] border-[#333]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">
|
||||
Configure MCP Server
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
Select a MCP server and configure its tools.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="mcp-select" className="text-neutral-300">
|
||||
MCP Server
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedMCP?.id}
|
||||
onValueChange={handleSelectMCP}
|
||||
>
|
||||
<SelectTrigger className="bg-[#222] border-[#444] text-white">
|
||||
<SelectValue placeholder="Select a MCP server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#222] border-[#444] text-white">
|
||||
{availableMCPs.map((mcp) => (
|
||||
<SelectItem
|
||||
key={mcp.id}
|
||||
value={mcp.id}
|
||||
className="data-[selected]:bg-[#333] data-[highlighted]:bg-[#333] !text-white focus:!text-white hover:text-emerald-400 data-[selected]:!text-emerald-400"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Server className="h-4 w-4 text-emerald-400" />
|
||||
{mcp.name}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{selectedMCP && (
|
||||
<>
|
||||
<div className="border border-[#444] rounded-md p-3 bg-[#222]">
|
||||
<p className="font-medium text-white">{selectedMCP.name}</p>
|
||||
<p className="text-sm text-neutral-400">
|
||||
{selectedMCP.description?.substring(0, 100)}...
|
||||
</p>
|
||||
<div className="mt-2 text-xs text-neutral-400">
|
||||
<p>
|
||||
<strong>Type:</strong> {selectedMCP.type}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Configuration:</strong>{" "}
|
||||
{selectedMCP.config_type === "sse" ? "SSE" : "Studio"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedMCP.environments &&
|
||||
Object.keys(selectedMCP.environments).length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-white">
|
||||
Environment Variables
|
||||
</h3>
|
||||
{Object.entries(selectedMCP.environments || {}).map(
|
||||
([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="grid grid-cols-3 items-center gap-4"
|
||||
>
|
||||
<Label
|
||||
htmlFor={`env-${key}`}
|
||||
className="text-right text-neutral-300"
|
||||
>
|
||||
{key}
|
||||
</Label>
|
||||
<Input
|
||||
id={`env-${key}`}
|
||||
value={mcpEnvs[key] || ""}
|
||||
onChange={(e) =>
|
||||
setMcpEnvs({
|
||||
...mcpEnvs,
|
||||
[key]: e.target.value,
|
||||
})
|
||||
}
|
||||
className="col-span-2 bg-[#222] border-[#444] text-white"
|
||||
placeholder={value as string}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedMCP.tools && selectedMCP.tools.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-white">
|
||||
Available Tools
|
||||
</h3>
|
||||
<div className="border border-[#444] rounded-md p-3 bg-[#222]">
|
||||
{selectedMCP.tools.map((tool: any) => (
|
||||
<div
|
||||
key={tool.id}
|
||||
className="flex items-center space-x-2 py-1"
|
||||
>
|
||||
<Checkbox
|
||||
id={`tool-${tool.id}`}
|
||||
checked={selectedMCPTools.includes(tool.id)}
|
||||
onCheckedChange={() => toggleMCPTool(tool.id)}
|
||||
className="data-[state=checked]:bg-emerald-400 data-[state=checked]:border-emerald-400"
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`tool-${tool.id}`}
|
||||
className="text-sm text-neutral-300"
|
||||
>
|
||||
{tool.name}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="p-4 pt-2 border-t border-[#333]">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
disabled={!selectedMCP}
|
||||
>
|
||||
Add MCP
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
136
frontend/app/agents/dialogs/MoveAgentDialog.tsx
Normal file
136
frontend/app/agents/dialogs/MoveAgentDialog.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/dialogs/MoveAgentDialog.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { Folder, Home } from "lucide-react";
|
||||
|
||||
interface Folder {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface MoveAgentDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
agent: Agent | null;
|
||||
folders: Folder[];
|
||||
onMove: (folderId: string | null) => Promise<void>;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function MoveAgentDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
agent,
|
||||
folders,
|
||||
onMove,
|
||||
isLoading = false,
|
||||
}: MoveAgentDialogProps) {
|
||||
const handleMove = async (folderId: string | null) => {
|
||||
await onMove(folderId);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[500px] bg-[#1a1a1a] border-[#333] text-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Move Agent</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
Choose a folder to move the agent "{agent?.name}"
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4">
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
className="w-full text-left px-4 py-3 rounded-md flex items-center bg-[#222] border border-[#444] hover:bg-[#333] hover:border-emerald-400/50 transition-colors"
|
||||
onClick={() => handleMove(null)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Home className="h-5 w-5 mr-3 text-neutral-400" />
|
||||
<div>
|
||||
<div className="font-medium">Remove from folder</div>
|
||||
<p className="text-sm text-neutral-400">
|
||||
The agent will be visible in "All agents"
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{folders.map((folder) => (
|
||||
<button
|
||||
key={folder.id}
|
||||
className="w-full text-left px-4 py-3 rounded-md flex items-center bg-[#222] border border-[#444] hover:bg-[#333] hover:border-emerald-400/50 transition-colors"
|
||||
onClick={() => handleMove(folder.id)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Folder className="h-5 w-5 mr-3 text-emerald-400" />
|
||||
<div>
|
||||
<div className="font-medium">{folder.name}</div>
|
||||
{folder.description && (
|
||||
<p className="text-sm text-neutral-400 truncate">
|
||||
{folder.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{isLoading && (
|
||||
<div className="flex items-center">
|
||||
<div className="animate-spin h-4 w-4 border-2 border-emerald-400 border-t-transparent rounded-full mr-2"></div>
|
||||
<span className="text-neutral-400">Moving...</span>
|
||||
</div>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
172
frontend/app/agents/dialogs/ShareAgentDialog.tsx
Normal file
172
frontend/app/agents/dialogs/ShareAgentDialog.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/dialogs/ShareAgentDialog.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Copy, Share2, ExternalLink } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
interface ShareAgentDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
agent: Agent;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export function ShareAgentDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
agent,
|
||||
apiKey,
|
||||
}: ShareAgentDialogProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [shareLink, setShareLink] = useState("");
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
if (open && agent && apiKey) {
|
||||
const baseUrl = window.location.origin;
|
||||
setShareLink(`${baseUrl}/shared-chat?agent=${agent.id}&key=${apiKey}`);
|
||||
}
|
||||
}, [open, agent, apiKey]);
|
||||
|
||||
const handleCopyLink = () => {
|
||||
if (shareLink) {
|
||||
navigator.clipboard.writeText(shareLink);
|
||||
setCopied(true);
|
||||
toast({
|
||||
title: "Link copied!",
|
||||
description: "The share link has been copied to the clipboard.",
|
||||
});
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyApiKey = () => {
|
||||
if (apiKey) {
|
||||
navigator.clipboard.writeText(apiKey);
|
||||
setCopied(true);
|
||||
toast({
|
||||
title: "API Key copied!",
|
||||
description: "The API Key has been copied to the clipboard.",
|
||||
});
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[500px] bg-[#1a1a1a] border-[#333] text-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Share2 className="h-5 w-5 text-emerald-400" />
|
||||
Share Agent
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
Share this agent with others without the need to login.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium text-white">Share Link</h3>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
readOnly
|
||||
value={shareLink}
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="shrink-0 bg-[#222] border-[#444] hover:bg-[#333] text-emerald-400 hover:text-emerald-300"
|
||||
onClick={handleCopyLink}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-400">
|
||||
Any person with this link can access the agent using the included API key.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium text-white">API Key</h3>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
readOnly
|
||||
value={apiKey}
|
||||
type="password"
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="shrink-0 bg-[#222] border-[#444] hover:bg-[#333] text-emerald-400 hover:text-emerald-300"
|
||||
onClick={handleCopyApiKey}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-400">
|
||||
The API key allows access to the agent. Do not share with untrusted people.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="border-t border-[#333] pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => window.open(shareLink, "_blank")}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white flex gap-2"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
Open Link
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
331
frontend/app/agents/forms/AgentForm.tsx
Normal file
331
frontend/app/agents/forms/AgentForm.tsx
Normal file
@@ -0,0 +1,331 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/forms/AgentForm.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { ApiKey } from "@/services/agentService";
|
||||
import { MCPServer } from "@/types/mcpServer";
|
||||
import { useState, useEffect } from "react";
|
||||
import { BasicInfoTab } from "./BasicInfoTab";
|
||||
import { ConfigurationTab } from "./ConfigurationTab";
|
||||
import { SubAgentsTab } from "./SubAgentsTab";
|
||||
import { MCPDialog } from "../dialogs/MCPDialog";
|
||||
import { CustomMCPDialog } from "../dialogs/CustomMCPDialog";
|
||||
|
||||
interface ModelOption {
|
||||
value: string;
|
||||
label: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
interface AgentFormProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
initialValues: Partial<Agent>;
|
||||
apiKeys: ApiKey[];
|
||||
availableModels: ModelOption[];
|
||||
availableMCPs: MCPServer[];
|
||||
agents: Agent[];
|
||||
onOpenApiKeysDialog: () => void;
|
||||
onOpenMCPDialog: (mcp?: any) => void;
|
||||
onOpenCustomMCPDialog: (customMCP?: any) => void;
|
||||
onSave: (values: Partial<Agent>) => Promise<void>;
|
||||
isLoading?: boolean;
|
||||
getAgentNameById: (id: string) => string;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
export function AgentForm({
|
||||
open,
|
||||
onOpenChange,
|
||||
initialValues,
|
||||
apiKeys,
|
||||
availableModels,
|
||||
availableMCPs,
|
||||
agents,
|
||||
onOpenApiKeysDialog,
|
||||
onOpenMCPDialog: externalOnOpenMCPDialog,
|
||||
onOpenCustomMCPDialog: externalOnOpenCustomMCPDialog,
|
||||
onSave,
|
||||
isLoading = false,
|
||||
getAgentNameById,
|
||||
clientId,
|
||||
}: AgentFormProps) {
|
||||
const [values, setValues] = useState<Partial<Agent>>(initialValues);
|
||||
const [activeTab, setActiveTab] = useState("basic");
|
||||
|
||||
const [mcpDialogOpen, setMcpDialogOpen] = useState(false);
|
||||
const [selectedMCP, setSelectedMCP] = useState<any>(null);
|
||||
const [customMcpDialogOpen, setCustomMcpDialogOpen] = useState(false);
|
||||
const [selectedCustomMCP, setSelectedCustomMCP] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setValues(initialValues);
|
||||
setActiveTab("basic");
|
||||
}
|
||||
}, [open, initialValues]);
|
||||
|
||||
const handleOpenMCPDialog = (mcpConfig: any = null) => {
|
||||
setSelectedMCP(mcpConfig);
|
||||
setMcpDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleOpenCustomMCPDialog = (customMCP: any = null) => {
|
||||
setSelectedCustomMCP(customMCP);
|
||||
setCustomMcpDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleConfigureMCP = (mcpConfig: any) => {
|
||||
handleOpenMCPDialog(mcpConfig);
|
||||
};
|
||||
|
||||
const handleRemoveMCP = (mcpId: string) => {
|
||||
setValues({
|
||||
...values,
|
||||
config: {
|
||||
...values.config,
|
||||
mcp_servers:
|
||||
values.config?.mcp_servers?.filter((mcp) => mcp.id !== mcpId) || [],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfigureCustomMCP = (customMCP: any) => {
|
||||
handleOpenCustomMCPDialog(customMCP);
|
||||
};
|
||||
|
||||
const handleRemoveCustomMCP = (url: string) => {
|
||||
setValues({
|
||||
...values,
|
||||
config: {
|
||||
...values.config,
|
||||
custom_mcp_servers:
|
||||
values.config?.custom_mcp_servers?.filter(
|
||||
(customMCP) => customMCP.url !== url
|
||||
) || [],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveMCP = (mcpConfig: any) => {
|
||||
const updatedMcpServers = [...(values.config?.mcp_servers || [])];
|
||||
const existingIndex = updatedMcpServers.findIndex(
|
||||
(mcp) => mcp.id === mcpConfig.id
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
updatedMcpServers[existingIndex] = mcpConfig;
|
||||
} else {
|
||||
updatedMcpServers.push(mcpConfig);
|
||||
}
|
||||
|
||||
setValues({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
mcp_servers: updatedMcpServers,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveCustomMCP = (customMCP: any) => {
|
||||
const updatedCustomMCPs = [...(values.config?.custom_mcp_servers || [])];
|
||||
const existingIndex = updatedCustomMCPs.findIndex(
|
||||
(mcp) => mcp.url === customMCP.url
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
updatedCustomMCPs[existingIndex] = customMCP;
|
||||
} else {
|
||||
updatedCustomMCPs.push(customMCP);
|
||||
}
|
||||
|
||||
setValues({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
custom_mcp_servers: updatedCustomMCPs,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const finalValues = {
|
||||
...values,
|
||||
client_id: clientId,
|
||||
name: values.name,
|
||||
};
|
||||
|
||||
await onSave(finalValues);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-hidden flex flex-col bg-[#1a1a1a] border-[#333]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">
|
||||
{initialValues.id ? "Edit Agent" : "New Agent"}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
{initialValues.id
|
||||
? "Edit the existing agent information"
|
||||
: "Fill in the information to create a new agent"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="flex-1 overflow-hidden flex flex-col"
|
||||
>
|
||||
<TabsList className="grid grid-cols-3 bg-[#222]">
|
||||
<TabsTrigger
|
||||
value="basic"
|
||||
className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400"
|
||||
>
|
||||
Basic Information
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="config"
|
||||
className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400"
|
||||
>
|
||||
Configuration
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="subagents"
|
||||
className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400"
|
||||
>
|
||||
Sub-Agents
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<ScrollArea className="flex-1 overflow-auto">
|
||||
<TabsContent value="basic" className="p-4 space-y-4">
|
||||
<BasicInfoTab
|
||||
values={values}
|
||||
onChange={setValues}
|
||||
apiKeys={apiKeys}
|
||||
availableModels={availableModels}
|
||||
onOpenApiKeysDialog={onOpenApiKeysDialog}
|
||||
clientId={clientId}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="config" className="p-4 space-y-4">
|
||||
<ConfigurationTab
|
||||
values={values}
|
||||
onChange={setValues}
|
||||
agents={agents}
|
||||
availableMCPs={availableMCPs}
|
||||
apiKeys={apiKeys}
|
||||
availableModels={availableModels}
|
||||
getAgentNameById={getAgentNameById}
|
||||
onOpenApiKeysDialog={onOpenApiKeysDialog}
|
||||
onConfigureMCP={handleConfigureMCP}
|
||||
onRemoveMCP={handleRemoveMCP}
|
||||
onConfigureCustomMCP={handleConfigureCustomMCP}
|
||||
onRemoveCustomMCP={handleRemoveCustomMCP}
|
||||
onOpenMCPDialog={handleOpenMCPDialog}
|
||||
onOpenCustomMCPDialog={handleOpenCustomMCPDialog}
|
||||
clientId={clientId}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="subagents" className="p-4 space-y-4">
|
||||
<SubAgentsTab
|
||||
values={values}
|
||||
onChange={setValues}
|
||||
getAgentNameById={getAgentNameById}
|
||||
editingAgentId={initialValues.id}
|
||||
clientId={clientId}
|
||||
/>
|
||||
</TabsContent>
|
||||
</ScrollArea>
|
||||
</Tabs>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d]"
|
||||
disabled={!values.name || isLoading}
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="animate-spin h-4 w-4 border-2 border-black border-t-transparent rounded-full mr-2"></div>
|
||||
)}
|
||||
{initialValues.id ? "Save Changes" : "Add Agent"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* MCP Dialog */}
|
||||
<MCPDialog
|
||||
open={mcpDialogOpen}
|
||||
onOpenChange={setMcpDialogOpen}
|
||||
onSave={handleSaveMCP}
|
||||
availableMCPs={availableMCPs}
|
||||
selectedMCP={
|
||||
availableMCPs.find((m) => selectedMCP?.id === m.id) || null
|
||||
}
|
||||
initialEnvs={selectedMCP?.envs || {}}
|
||||
initialTools={selectedMCP?.tools || []}
|
||||
clientId={clientId}
|
||||
/>
|
||||
|
||||
{/* Custom MCP Dialog */}
|
||||
<CustomMCPDialog
|
||||
open={customMcpDialogOpen}
|
||||
onOpenChange={setCustomMcpDialogOpen}
|
||||
onSave={handleSaveCustomMCP}
|
||||
initialCustomMCP={selectedCustomMCP}
|
||||
clientId={clientId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
243
frontend/app/agents/forms/BasicInfoTab.tsx
Normal file
243
frontend/app/agents/forms/BasicInfoTab.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/forms/BasicInfoTab.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { AgentTypeSelector } from "@/app/agents/AgentTypeSelector";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Agent, AgentType } from "@/types/agent";
|
||||
import { ApiKey } from "@/services/agentService";
|
||||
import { A2AAgentConfig } from "../config/A2AAgentConfig";
|
||||
import { LLMAgentConfig } from "../config/LLMAgentConfig";
|
||||
import { sanitizeAgentName } from "@/lib/utils";
|
||||
|
||||
interface ModelOption {
|
||||
value: string;
|
||||
label: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
interface BasicInfoTabProps {
|
||||
values: Partial<Agent>;
|
||||
onChange: (values: Partial<Agent>) => void;
|
||||
apiKeys: ApiKey[];
|
||||
availableModels: ModelOption[];
|
||||
onOpenApiKeysDialog: () => void;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
export function BasicInfoTab({
|
||||
values,
|
||||
onChange,
|
||||
apiKeys,
|
||||
availableModels,
|
||||
onOpenApiKeysDialog,
|
||||
}: BasicInfoTabProps) {
|
||||
const handleNameBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
const sanitizedName = sanitizeAgentName(e.target.value);
|
||||
if (sanitizedName !== e.target.value) {
|
||||
onChange({ ...values, name: sanitizedName });
|
||||
}
|
||||
};
|
||||
|
||||
const handleTypeChange = (type: AgentType) => {
|
||||
let newValues: Partial<Agent> = { ...values, type };
|
||||
|
||||
if (type === "llm") {
|
||||
newValues = {
|
||||
...newValues,
|
||||
model: "openai/gpt-4.1-nano",
|
||||
instruction: "",
|
||||
role: "",
|
||||
goal: "",
|
||||
agent_card_url: undefined,
|
||||
config: {
|
||||
tools: [],
|
||||
mcp_servers: [],
|
||||
custom_mcp_servers: [],
|
||||
custom_tools: {
|
||||
http_tools: [],
|
||||
},
|
||||
sub_agents: [],
|
||||
},
|
||||
};
|
||||
} else if (type === "a2a") {
|
||||
newValues = {
|
||||
...newValues,
|
||||
model: undefined,
|
||||
instruction: undefined,
|
||||
role: undefined,
|
||||
goal: undefined,
|
||||
agent_card_url: "",
|
||||
api_key_id: undefined,
|
||||
config: undefined,
|
||||
};
|
||||
} else if (type === "loop") {
|
||||
newValues = {
|
||||
...newValues,
|
||||
model: undefined,
|
||||
instruction: undefined,
|
||||
role: undefined,
|
||||
goal: undefined,
|
||||
agent_card_url: undefined,
|
||||
api_key_id: undefined,
|
||||
config: {
|
||||
sub_agents: [],
|
||||
custom_mcp_servers: [],
|
||||
},
|
||||
};
|
||||
} else if (type === "workflow") {
|
||||
newValues = {
|
||||
...newValues,
|
||||
model: undefined,
|
||||
instruction: undefined,
|
||||
role: undefined,
|
||||
goal: undefined,
|
||||
agent_card_url: undefined,
|
||||
api_key_id: undefined,
|
||||
config: {
|
||||
sub_agents: [],
|
||||
workflow: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
} else if (type === "task") {
|
||||
newValues = {
|
||||
...newValues,
|
||||
model: undefined,
|
||||
instruction: undefined,
|
||||
role: undefined,
|
||||
goal: undefined,
|
||||
agent_card_url: undefined,
|
||||
api_key_id: undefined,
|
||||
config: {
|
||||
tasks: [],
|
||||
sub_agents: [],
|
||||
},
|
||||
};
|
||||
} else {
|
||||
newValues = {
|
||||
...newValues,
|
||||
model: undefined,
|
||||
instruction: undefined,
|
||||
role: undefined,
|
||||
goal: undefined,
|
||||
agent_card_url: undefined,
|
||||
api_key_id: undefined,
|
||||
config: {
|
||||
sub_agents: [],
|
||||
custom_mcp_servers: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
onChange(newValues);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="type" className="text-right text-neutral-300">
|
||||
Agent Type
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
<AgentTypeSelector
|
||||
value={values.type || "llm"}
|
||||
onValueChange={handleTypeChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right text-neutral-300">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={values.name || ""}
|
||||
onChange={(e) => onChange({ ...values, name: e.target.value })}
|
||||
onBlur={handleNameBlur}
|
||||
className="col-span-3 bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{values.type !== "a2a" && (
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="description" className="text-right text-neutral-300">
|
||||
Description
|
||||
</Label>
|
||||
<Input
|
||||
id="description"
|
||||
value={values.description || ""}
|
||||
onChange={(e) =>
|
||||
onChange({ ...values, description: e.target.value })
|
||||
}
|
||||
className="col-span-3 bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{values.type === "llm" && (
|
||||
<LLMAgentConfig
|
||||
apiKeys={apiKeys}
|
||||
availableModels={availableModels}
|
||||
values={values}
|
||||
onChange={onChange}
|
||||
onOpenApiKeysDialog={onOpenApiKeysDialog}
|
||||
/>
|
||||
)}
|
||||
|
||||
{values.type === "loop" && values.config?.max_iterations && (
|
||||
<div className="space-y-1 text-xs text-neutral-400">
|
||||
<div>
|
||||
<strong>Max. Iterations:</strong> {values.config.max_iterations}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{values.type === "workflow" && (
|
||||
<div className="space-y-1 text-xs text-neutral-400">
|
||||
<div>
|
||||
<strong>Type:</strong> Visual Flow
|
||||
</div>
|
||||
{values.config?.workflow && (
|
||||
<div>
|
||||
<strong>Elements:</strong>{" "}
|
||||
{values.config.workflow.nodes?.length || 0} nodes,{" "}
|
||||
{values.config.workflow.edges?.length || 0} connections
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
722
frontend/app/agents/forms/ConfigurationTab.tsx
Normal file
722
frontend/app/agents/forms/ConfigurationTab.tsx
Normal file
@@ -0,0 +1,722 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/forms/ConfigurationTab.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { MCPServer } from "@/types/mcpServer";
|
||||
import {
|
||||
Check,
|
||||
Copy,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Plus,
|
||||
Server,
|
||||
Settings,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { ParallelAgentConfig } from "../config/ParallelAgentConfig";
|
||||
import { SequentialAgentConfig } from "../config/SequentialAgentConfig";
|
||||
import { ApiKey } from "@/services/agentService";
|
||||
import { LoopAgentConfig } from "../config/LoopAgentConfig copy";
|
||||
import { A2AAgentConfig } from "../config/A2AAgentConfig";
|
||||
import { TaskAgentConfig } from "../config/TaskAgentConfig";
|
||||
import { useState } from "react";
|
||||
import { MCPDialog } from "../dialogs/MCPDialog";
|
||||
import { CustomMCPDialog } from "../dialogs/CustomMCPDialog";
|
||||
import { AgentToolDialog } from "../dialogs/AgentToolDialog";
|
||||
import { CustomToolDialog } from "../dialogs/CustomToolDialog";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
interface ConfigurationTabProps {
|
||||
values: Partial<Agent>;
|
||||
onChange: (values: Partial<Agent>) => void;
|
||||
agents: Agent[];
|
||||
availableMCPs: MCPServer[];
|
||||
apiKeys: ApiKey[];
|
||||
availableModels: any[];
|
||||
getAgentNameById: (id: string) => string;
|
||||
onOpenApiKeysDialog: () => void;
|
||||
onConfigureMCP: (mcpConfig: any) => void;
|
||||
onRemoveMCP: (mcpId: string) => void;
|
||||
onConfigureCustomMCP: (customMCP: any) => void;
|
||||
onRemoveCustomMCP: (url: string) => void;
|
||||
onOpenMCPDialog: (mcpConfig?: any) => void;
|
||||
onOpenCustomMCPDialog: (customMCP?: any) => void;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
export function ConfigurationTab({
|
||||
values,
|
||||
onChange,
|
||||
agents,
|
||||
availableMCPs,
|
||||
apiKeys,
|
||||
availableModels,
|
||||
getAgentNameById,
|
||||
onOpenApiKeysDialog,
|
||||
onConfigureMCP,
|
||||
onRemoveMCP,
|
||||
onConfigureCustomMCP,
|
||||
onRemoveCustomMCP,
|
||||
onOpenMCPDialog,
|
||||
onOpenCustomMCPDialog,
|
||||
clientId,
|
||||
}: ConfigurationTabProps) {
|
||||
const [agentToolDialogOpen, setAgentToolDialogOpen] = useState(false);
|
||||
const [customToolDialogOpen, setCustomToolDialogOpen] = useState(false);
|
||||
const [editingCustomTool, setEditingCustomTool] = useState<any>(null);
|
||||
const [showApiKey, setShowApiKey] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [copiedCardUrl, setCopiedCardUrl] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleAddAgentTool = (tool: { id: string }) => {
|
||||
const updatedAgentTools = [...(values.config?.agent_tools || [])];
|
||||
if (!updatedAgentTools.includes(tool.id)) {
|
||||
updatedAgentTools.push(tool.id);
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
agent_tools: updatedAgentTools,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleRemoveAgentTool = (id: string) => {
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
agent_tools: (values.config?.agent_tools || []).filter(
|
||||
(toolId) => toolId !== id
|
||||
),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Custom Tools handlers
|
||||
const handleAddCustomTool = (tool: any) => {
|
||||
const updatedTools = [...(values.config?.custom_tools?.http_tools || [])];
|
||||
updatedTools.push(tool);
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
custom_tools: {
|
||||
...(values.config?.custom_tools || { http_tools: [] }),
|
||||
http_tools: updatedTools,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
const handleEditCustomTool = (tool: any, idx: number) => {
|
||||
setEditingCustomTool({ ...tool, idx });
|
||||
setCustomToolDialogOpen(true);
|
||||
};
|
||||
const handleSaveEditCustomTool = (tool: any) => {
|
||||
const updatedTools = [...(values.config?.custom_tools?.http_tools || [])];
|
||||
if (editingCustomTool && typeof editingCustomTool.idx === "number") {
|
||||
updatedTools[editingCustomTool.idx] = tool;
|
||||
}
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
custom_tools: {
|
||||
...(values.config?.custom_tools || { http_tools: [] }),
|
||||
http_tools: updatedTools,
|
||||
},
|
||||
},
|
||||
});
|
||||
setEditingCustomTool(null);
|
||||
};
|
||||
const handleRemoveCustomTool = (idx: number) => {
|
||||
const updatedTools = [...(values.config?.custom_tools?.http_tools || [])];
|
||||
updatedTools.splice(idx, 1);
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
custom_tools: {
|
||||
...(values.config?.custom_tools || { http_tools: [] }),
|
||||
http_tools: updatedTools,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const apiKeyField = (
|
||||
<div className="space-y-2 mb-4">
|
||||
<h3 className="text-lg font-medium text-white">Credentials</h3>
|
||||
<div className="border border-[#444] rounded-md p-4 bg-[#222] flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label
|
||||
className="text-sm text-neutral-400 mb-1"
|
||||
htmlFor="agent-card-url"
|
||||
>
|
||||
Agent URL. This URL can be used to access the agent card externally.
|
||||
</label>
|
||||
<div className="relative flex items-center">
|
||||
<input
|
||||
id="agent-card-url"
|
||||
type="text"
|
||||
className="w-full bg-[#2a2a2a] border border-[#444] rounded-md px-3 py-2 text-white pr-12 focus:outline-none focus:ring-2 focus:ring-emerald-400/40"
|
||||
value={
|
||||
values?.agent_card_url?.replace(
|
||||
"/.well-known/agent.json",
|
||||
""
|
||||
) || ""
|
||||
}
|
||||
disabled
|
||||
autoComplete="off"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 text-neutral-400 hover:text-emerald-400 px-1 py-1"
|
||||
onClick={async () => {
|
||||
if (values?.agent_card_url) {
|
||||
await navigator.clipboard.writeText(
|
||||
values.agent_card_url.replace("/.well-known/agent.json", "")
|
||||
);
|
||||
setCopiedCardUrl(true);
|
||||
setTimeout(() => setCopiedCardUrl(false), 1200);
|
||||
toast({
|
||||
title: "Copied!",
|
||||
description:
|
||||
"The agent URL was copied to the clipboard.",
|
||||
});
|
||||
}
|
||||
}}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{copiedCardUrl ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm text-neutral-400 mb-1" htmlFor="agent-api_key">
|
||||
Configure the API Key for this agent. This key will be used for
|
||||
authentication with external services.
|
||||
</label>
|
||||
<div className="relative flex items-center">
|
||||
<input
|
||||
id="agent-api_key"
|
||||
type={showApiKey ? "text" : "password"}
|
||||
className="w-full bg-[#2a2a2a] border border-[#444] rounded-md px-3 py-2 text-white pr-24 focus:outline-none focus:ring-2 focus:ring-emerald-400/40"
|
||||
value={values.config?.api_key || ""}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...(values.config || {}),
|
||||
api_key: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-9 text-neutral-400 hover:text-emerald-400 px-1 py-1"
|
||||
onClick={() => setShowApiKey((v) => !v)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showApiKey ? (
|
||||
<EyeOff className="w-4 h-4" />
|
||||
) : (
|
||||
<Eye className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 text-neutral-400 hover:text-emerald-400 px-1 py-1"
|
||||
onClick={async () => {
|
||||
if (values.config?.api_key) {
|
||||
await navigator.clipboard.writeText(values.config.api_key);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1200);
|
||||
toast({
|
||||
title: "Copied!",
|
||||
description:
|
||||
"The API key was copied to the clipboard.",
|
||||
});
|
||||
}
|
||||
}}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (values.type === "llm") {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{apiKeyField}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium text-white">MCP Servers</h3>
|
||||
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||
<p className="text-sm text-neutral-400 mb-4">
|
||||
Configure the MCP servers that this agent can use.
|
||||
</p>
|
||||
|
||||
{values.config?.mcp_servers &&
|
||||
values.config.mcp_servers.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{values.config.mcp_servers.map((mcpConfig) => {
|
||||
const mcpServer = availableMCPs.find(
|
||||
(mcp) => mcp.id === mcpConfig.id
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={mcpConfig.id}
|
||||
className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium text-white">
|
||||
{mcpServer?.name || mcpConfig.id}
|
||||
</p>
|
||||
<p className="text-sm text-neutral-400">
|
||||
{mcpServer?.description?.substring(0, 100)}
|
||||
...
|
||||
</p>
|
||||
{mcpConfig.tools && mcpConfig.tools.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{mcpConfig.tools.map((toolId) => (
|
||||
<Badge
|
||||
key={toolId}
|
||||
variant="outline"
|
||||
className="text-xs bg-[#333] text-emerald-400 border-emerald-400/30"
|
||||
>
|
||||
{toolId}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onConfigureMCP(mcpConfig)}
|
||||
className="flex items-center text-neutral-300 hover:text-emerald-400 hover:bg-[#333]"
|
||||
>
|
||||
<Settings className="h-4 w-4 mr-1" /> Configure
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onRemoveMCP(mcpConfig.id)}
|
||||
className="text-red-500 hover:text-red-400 hover:bg-[#333]"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onOpenMCPDialog(null)}
|
||||
className="w-full mt-2 border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add MCP Server
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md mb-2">
|
||||
<div>
|
||||
<p className="font-medium text-white">
|
||||
No MCP servers configured
|
||||
</p>
|
||||
<p className="text-sm text-neutral-400">
|
||||
Add MCP servers for this agent
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onOpenMCPDialog(null)}
|
||||
className="border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium text-white">Custom MCPs</h3>
|
||||
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||
<p className="text-sm text-neutral-400 mb-4">
|
||||
Configure custom MCPs with URL and HTTP headers.
|
||||
</p>
|
||||
|
||||
{values.config?.custom_mcp_servers &&
|
||||
values.config.custom_mcp_servers.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{values.config.custom_mcp_servers.map((customMCP) => (
|
||||
<div
|
||||
key={customMCP.url}
|
||||
className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium text-white">{customMCP.url}</p>
|
||||
<p className="text-sm text-neutral-400">
|
||||
{Object.keys(customMCP.headers || {}).length > 0
|
||||
? `${
|
||||
Object.keys(customMCP.headers || {}).length
|
||||
} headers configured`
|
||||
: "No headers configured"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onConfigureCustomMCP(customMCP)}
|
||||
className="flex items-center text-neutral-300 hover:text-emerald-400 hover:bg-[#333]"
|
||||
>
|
||||
<Settings className="h-4 w-4 mr-1" /> Configure
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onRemoveCustomMCP(customMCP.url)}
|
||||
className="text-red-500 hover:text-red-400 hover:bg-[#333]"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onOpenCustomMCPDialog(null)}
|
||||
className="w-full mt-2 border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add Custom MCP
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md mb-2">
|
||||
<div>
|
||||
<p className="font-medium text-white">
|
||||
No custom MCPs configured
|
||||
</p>
|
||||
<p className="text-sm text-neutral-400">
|
||||
Add custom MCPs for this agent
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onOpenCustomMCPDialog(null)}
|
||||
className="border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium text-white">Agent Tools</h3>
|
||||
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||
<p className="text-sm text-neutral-400 mb-4">
|
||||
Configure other agents as tools for this agent.
|
||||
</p>
|
||||
{values.config?.agent_tools &&
|
||||
values.config.agent_tools.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{values.config.agent_tools.map((toolId) => {
|
||||
const agent = agents.find((a) => a.id === toolId);
|
||||
return (
|
||||
<div
|
||||
key={toolId}
|
||||
className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium text-white">
|
||||
{agent?.name || toolId}
|
||||
</p>
|
||||
<p className="text-sm text-neutral-400">
|
||||
{agent?.description || "No description"}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveAgentTool(toolId)}
|
||||
className="text-red-500 hover:text-red-400 hover:bg-[#333]"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setAgentToolDialogOpen(true)}
|
||||
className="w-full mt-2 border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add Agent Tool
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md mb-2">
|
||||
<div>
|
||||
<p className="font-medium text-white">
|
||||
No agent tools configured
|
||||
</p>
|
||||
<p className="text-sm text-neutral-400">
|
||||
Add agent tools for this agent
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setAgentToolDialogOpen(true)}
|
||||
className="border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium text-white">
|
||||
Custom Tools (HTTP Tools)
|
||||
</h3>
|
||||
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||
<p className="text-sm text-neutral-400 mb-4">
|
||||
Configure HTTP tools for this agent.
|
||||
</p>
|
||||
{values.config?.custom_tools?.http_tools &&
|
||||
values.config.custom_tools.http_tools.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{values.config.custom_tools.http_tools.map((tool, idx) => (
|
||||
<div
|
||||
key={tool.name + idx}
|
||||
className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium text-white">{tool.name}</p>
|
||||
<p className="text-xs text-neutral-400">
|
||||
{tool.method} {tool.endpoint}
|
||||
</p>
|
||||
<p className="text-xs text-neutral-400">
|
||||
{tool.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditCustomTool(tool, idx)}
|
||||
className="flex items-center text-neutral-300 hover:text-emerald-400 hover:bg-[#333]"
|
||||
>
|
||||
<span className="mr-1">Edit</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveCustomTool(idx)}
|
||||
className="text-red-500 hover:text-red-400 hover:bg-[#333]"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setEditingCustomTool(null);
|
||||
setCustomToolDialogOpen(true);
|
||||
}}
|
||||
className="w-full mt-2 border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add Custom Tool
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between p-2 bg-[#2a2a2a] rounded-md mb-2">
|
||||
<div>
|
||||
<p className="font-medium text-white">
|
||||
No custom tools configured
|
||||
</p>
|
||||
<p className="text-sm text-neutral-400">
|
||||
Add HTTP tools for this agent
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setEditingCustomTool(null);
|
||||
setCustomToolDialogOpen(true);
|
||||
}}
|
||||
className="border-emerald-400 text-emerald-400 hover:bg-emerald-400/10 bg-[#222] hover:text-emerald-400"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> Add
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<CustomToolDialog
|
||||
open={customToolDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setCustomToolDialogOpen(open);
|
||||
if (!open) setEditingCustomTool(null);
|
||||
}}
|
||||
onSave={
|
||||
editingCustomTool ? handleSaveEditCustomTool : handleAddCustomTool
|
||||
}
|
||||
initialTool={editingCustomTool}
|
||||
/>
|
||||
{agentToolDialogOpen && (
|
||||
<AgentToolDialog
|
||||
open={agentToolDialogOpen}
|
||||
onOpenChange={setAgentToolDialogOpen}
|
||||
onSave={handleAddAgentTool}
|
||||
currentAgentId={values.id}
|
||||
folderId={values.folder_id}
|
||||
clientId={clientId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (values.type === "a2a") {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{apiKeyField}
|
||||
<A2AAgentConfig values={values} onChange={onChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (values.type === "sequential") {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{apiKeyField}
|
||||
<SequentialAgentConfig
|
||||
values={values}
|
||||
onChange={onChange}
|
||||
agents={agents}
|
||||
getAgentNameById={getAgentNameById}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (values.type === "parallel") {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{apiKeyField}
|
||||
<ParallelAgentConfig
|
||||
values={values}
|
||||
onChange={onChange}
|
||||
agents={agents}
|
||||
getAgentNameById={getAgentNameById}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (values.type === "loop") {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{apiKeyField}
|
||||
<LoopAgentConfig
|
||||
values={values}
|
||||
onChange={onChange}
|
||||
agents={agents}
|
||||
getAgentNameById={getAgentNameById}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (values.type === "task") {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{apiKeyField}
|
||||
<TaskAgentConfig
|
||||
values={values}
|
||||
onChange={onChange}
|
||||
agents={agents}
|
||||
getAgentNameById={getAgentNameById}
|
||||
singleTask={values.type === "task"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{apiKeyField}
|
||||
<div className="flex items-center justify-center h-40">
|
||||
<div className="text-center">
|
||||
<p className="text-neutral-400">
|
||||
Configure the sub-agents in the "Sub-Agents" tab
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
246
frontend/app/agents/forms/SubAgentsTab.tsx
Normal file
246
frontend/app/agents/forms/SubAgentsTab.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/forms/SubAgentsTab.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { listAgents } from "@/services/agentService";
|
||||
import { Loader2, Search, X } from "lucide-react";
|
||||
|
||||
interface SubAgentsTabProps {
|
||||
values: Partial<Agent>;
|
||||
onChange: (values: Partial<Agent>) => void;
|
||||
getAgentNameById: (id: string) => string;
|
||||
editingAgentId?: string;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
export function SubAgentsTab({
|
||||
values,
|
||||
onChange,
|
||||
getAgentNameById,
|
||||
editingAgentId,
|
||||
clientId,
|
||||
}: SubAgentsTabProps) {
|
||||
const [availableAgents, setAvailableAgents] = useState<Agent[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
// Get folder ID from current agent
|
||||
const folderId = values.folder_id;
|
||||
|
||||
useEffect(() => {
|
||||
loadAgents();
|
||||
}, [clientId, folderId, editingAgentId]);
|
||||
|
||||
const loadAgents = async () => {
|
||||
if (!clientId) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await listAgents(
|
||||
clientId,
|
||||
0,
|
||||
100,
|
||||
folderId || undefined
|
||||
);
|
||||
|
||||
// Filter out the current agent to avoid self-reference
|
||||
const filteredAgents = res.data.filter(agent =>
|
||||
agent.id !== editingAgentId
|
||||
);
|
||||
|
||||
setAvailableAgents(filteredAgents);
|
||||
} catch (error) {
|
||||
console.error("Error loading agents:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddSubAgent = (agentId: string) => {
|
||||
if (!values.config?.sub_agents?.includes(agentId)) {
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...values.config,
|
||||
sub_agents: [...(values.config?.sub_agents || []), agentId],
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveSubAgent = (agentId: string) => {
|
||||
onChange({
|
||||
...values,
|
||||
config: {
|
||||
...values.config,
|
||||
sub_agents:
|
||||
values.config?.sub_agents?.filter((id) => id !== agentId) || [],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const filteredAgents = availableAgents.filter(agent =>
|
||||
agent.name.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-medium text-white">Sub-Agents</h3>
|
||||
<div className="text-sm text-neutral-400">
|
||||
{values.config?.sub_agents?.length || 0} sub-agents selected
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-[#444] rounded-md p-4 bg-[#222]">
|
||||
<p className="text-sm text-neutral-400 mb-4">
|
||||
Select the agents that will be used as sub-agents.
|
||||
</p>
|
||||
|
||||
{values.config?.sub_agents && values.config.sub_agents.length > 0 ? (
|
||||
<div className="space-y-2 mb-4">
|
||||
<h4 className="text-sm font-medium text-white">
|
||||
Selected sub-agents:
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{values.config.sub_agents.map((agentId) => (
|
||||
<Badge
|
||||
key={agentId}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-1 bg-[#333] text-emerald-400"
|
||||
>
|
||||
{getAgentNameById(agentId)}
|
||||
<button
|
||||
onClick={() => handleRemoveSubAgent(agentId)}
|
||||
className="ml-1 h-4 w-4 rounded-full hover:bg-[#444] inline-flex items-center justify-center"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-4 text-neutral-400 mb-4">
|
||||
No sub-agents selected
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-4">
|
||||
<h4 className="text-sm font-medium text-white mb-2">
|
||||
Available agents:
|
||||
</h4>
|
||||
<div className="relative mb-3">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-neutral-500" />
|
||||
<Input
|
||||
placeholder="Search agents by name..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="pl-9 bg-[#1a1a1a] border-[#444] text-white"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
onClick={() => setSearch("")}
|
||||
className="absolute right-2.5 top-2.5 text-neutral-400 hover:text-white"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-6">
|
||||
<Loader2 className="h-6 w-6 text-emerald-400 animate-spin" />
|
||||
<div className="mt-2 text-sm text-neutral-400">Loading agents...</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 max-h-60 overflow-y-auto">
|
||||
{filteredAgents.length === 0 ? (
|
||||
<div className="text-center py-4 text-neutral-400">
|
||||
{search ? `No agents found matching "${search}"` : "No other agents found in this folder"}
|
||||
</div>
|
||||
) : (
|
||||
filteredAgents.map((agent) => (
|
||||
<div
|
||||
key={agent.id}
|
||||
className="flex items-center justify-between p-2 hover:bg-[#2a2a2a] rounded-md"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-white">{agent.name}</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="ml-2 border-[#444] text-emerald-400"
|
||||
>
|
||||
{agent.type === "llm"
|
||||
? "LLM Agent"
|
||||
: agent.type === "a2a"
|
||||
? "A2A Agent"
|
||||
: agent.type === "sequential"
|
||||
? "Sequential Agent"
|
||||
: agent.type === "parallel"
|
||||
? "Parallel Agent"
|
||||
: agent.type === "loop"
|
||||
? "Loop Agent"
|
||||
: agent.type === "workflow"
|
||||
? "Workflow Agent"
|
||||
: agent.type === "task"
|
||||
? "Task Agent"
|
||||
: agent.type}
|
||||
</Badge>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleAddSubAgent(agent.id)}
|
||||
disabled={values.config?.sub_agents?.includes(agent.id)}
|
||||
className={
|
||||
values.config?.sub_agents?.includes(agent.id)
|
||||
? "text-neutral-500 bg-[#222] hover:bg-[#333]"
|
||||
: "text-emerald-400 hover:bg-[#333] bg-[#222]"
|
||||
}
|
||||
>
|
||||
{values.config?.sub_agents?.includes(agent.id)
|
||||
? "Added"
|
||||
: "Add"}
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
754
frontend/app/agents/page.tsx
Normal file
754
frontend/app/agents/page.tsx
Normal file
@@ -0,0 +1,754 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/page.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Key, Plus, Folder, Download, Upload } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { exportAsJson } from "@/lib/utils";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
import { Agent, AgentCreate } from "@/types/agent";
|
||||
import { Folder as AgentFolder } from "@/services/agentService";
|
||||
import {
|
||||
listAgents,
|
||||
createAgent,
|
||||
updateAgent,
|
||||
deleteAgent,
|
||||
listFolders,
|
||||
createFolder,
|
||||
updateFolder,
|
||||
deleteFolder,
|
||||
assignAgentToFolder,
|
||||
ApiKey,
|
||||
listApiKeys,
|
||||
createApiKey,
|
||||
updateApiKey,
|
||||
deleteApiKey,
|
||||
shareAgent,
|
||||
importAgentFromJson,
|
||||
} from "@/services/agentService";
|
||||
import { listMCPServers } from "@/services/mcpServerService";
|
||||
import { AgentSidebar } from "./AgentSidebar";
|
||||
import { SearchInput } from "./SearchInput";
|
||||
import { AgentList } from "./AgentList";
|
||||
import { EmptyState } from "./EmptyState";
|
||||
import { AgentForm } from "./forms/AgentForm";
|
||||
import { FolderDialog } from "./dialogs/FolderDialog";
|
||||
import { MoveAgentDialog } from "./dialogs/MoveAgentDialog";
|
||||
import { ConfirmationDialog } from "./dialogs/ConfirmationDialog";
|
||||
import { ApiKeysDialog } from "./dialogs/ApiKeysDialog";
|
||||
import { ShareAgentDialog } from "./dialogs/ShareAgentDialog";
|
||||
import { MCPServer } from "@/types/mcpServer";
|
||||
import { availableModels } from "@/types/aiModels";
|
||||
import { ImportAgentDialog } from "./dialogs/ImportAgentDialog";
|
||||
|
||||
export default function AgentsPage() {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const user =
|
||||
typeof window !== "undefined"
|
||||
? JSON.parse(localStorage.getItem("user") || "{}")
|
||||
: {};
|
||||
const clientId = user?.client_id || "";
|
||||
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [filteredAgents, setFilteredAgents] = useState<Agent[]>([]);
|
||||
const [folders, setFolders] = useState<AgentFolder[]>([]);
|
||||
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
|
||||
const [availableMCPs, setAvailableMCPs] = useState<MCPServer[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [selectedAgentType, setSelectedAgentType] = useState<string | null>(null);
|
||||
const [agentTypes, setAgentTypes] = useState<string[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [isSidebarVisible, setIsSidebarVisible] = useState(false);
|
||||
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null);
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [isFolderDialogOpen, setIsFolderDialogOpen] = useState(false);
|
||||
const [isMovingDialogOpen, setIsMovingDialogOpen] = useState(false);
|
||||
const [isDeleteAgentDialogOpen, setIsDeleteAgentDialogOpen] = useState(false);
|
||||
const [isDeleteFolderDialogOpen, setIsDeleteFolderDialogOpen] =
|
||||
useState(false);
|
||||
const [isApiKeysDialogOpen, setIsApiKeysDialogOpen] = useState(false);
|
||||
const [isMCPDialogOpen, setIsMCPDialogOpen] = useState(false);
|
||||
const [isCustomMCPDialogOpen, setIsCustomMCPDialogOpen] = useState(false);
|
||||
const [isShareDialogOpen, setIsShareDialogOpen] = useState(false);
|
||||
|
||||
const [editingAgent, setEditingAgent] = useState<Agent | null>(null);
|
||||
const [editingFolder, setEditingFolder] = useState<AgentFolder | null>(null);
|
||||
const [agentToDelete, setAgentToDelete] = useState<Agent | null>(null);
|
||||
const [agentToMove, setAgentToMove] = useState<Agent | null>(null);
|
||||
const [agentToShare, setAgentToShare] = useState<Agent | null>(null);
|
||||
const [sharedApiKey, setSharedApiKey] = useState<string>("");
|
||||
const [folderToDelete, setFolderToDelete] = useState<AgentFolder | null>(null);
|
||||
|
||||
const [newAgent, setNewAgent] = useState<Partial<Agent>>({
|
||||
client_id: clientId || "",
|
||||
name: "",
|
||||
description: "",
|
||||
type: "llm",
|
||||
model: "openai/gpt-4.1-nano",
|
||||
instruction: "",
|
||||
api_key_id: "",
|
||||
config: {
|
||||
tools: [],
|
||||
mcp_servers: [],
|
||||
custom_mcp_servers: [],
|
||||
custom_tools: {
|
||||
http_tools: [],
|
||||
},
|
||||
sub_agents: [],
|
||||
agent_tools: [],
|
||||
},
|
||||
});
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [isImportDialogOpen, setIsImportDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clientId) return;
|
||||
loadAgents();
|
||||
loadFolders();
|
||||
loadApiKeys();
|
||||
}, [clientId, selectedFolderId]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadMCPs = async () => {
|
||||
try {
|
||||
const res = await listMCPServers();
|
||||
setAvailableMCPs(res.data);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error loading MCP servers",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadMCPs();
|
||||
}, []);
|
||||
|
||||
const loadAgents = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await listAgents(
|
||||
clientId,
|
||||
0,
|
||||
100,
|
||||
selectedFolderId || undefined
|
||||
);
|
||||
setAgents(res.data);
|
||||
setFilteredAgents(res.data);
|
||||
|
||||
// Extract unique agent types
|
||||
const types = [...new Set(res.data.map(agent => agent.type))].filter(Boolean);
|
||||
setAgentTypes(types);
|
||||
} catch (error) {
|
||||
toast({ title: "Error loading agents", variant: "destructive" });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadFolders = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await listFolders(clientId);
|
||||
setFolders(res.data);
|
||||
} catch (error) {
|
||||
toast({ title: "Error loading folders", variant: "destructive" });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadApiKeys = async () => {
|
||||
try {
|
||||
const res = await listApiKeys(clientId);
|
||||
setApiKeys(res.data);
|
||||
} catch (error) {
|
||||
toast({ title: "Error loading API keys", variant: "destructive" });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Apply both search term and type filters
|
||||
let filtered = [...agents];
|
||||
|
||||
// Apply search term filter
|
||||
if (searchTerm.trim() !== "") {
|
||||
const lowercaseSearch = searchTerm.toLowerCase();
|
||||
filtered = filtered.filter(
|
||||
(agent) =>
|
||||
agent.name.toLowerCase().includes(lowercaseSearch) ||
|
||||
agent.description?.toLowerCase().includes(lowercaseSearch)
|
||||
);
|
||||
}
|
||||
|
||||
// Apply agent type filter
|
||||
if (selectedAgentType) {
|
||||
filtered = filtered.filter(agent => agent.type === selectedAgentType);
|
||||
}
|
||||
|
||||
setFilteredAgents(filtered);
|
||||
}, [searchTerm, selectedAgentType, agents]);
|
||||
|
||||
const handleAddAgent = async (agentData: Partial<Agent>) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
if (editingAgent) {
|
||||
await updateAgent(editingAgent.id, {
|
||||
...agentData,
|
||||
client_id: clientId,
|
||||
});
|
||||
toast({
|
||||
title: "Agent updated",
|
||||
description: `${agentData.name} was updated successfully`,
|
||||
});
|
||||
} else {
|
||||
const createdAgent = await createAgent({
|
||||
...(agentData as AgentCreate),
|
||||
client_id: clientId,
|
||||
});
|
||||
|
||||
if (selectedFolderId && createdAgent.data.id) {
|
||||
await assignAgentToFolder(
|
||||
createdAgent.data.id,
|
||||
selectedFolderId,
|
||||
clientId
|
||||
);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Agent added",
|
||||
description: `${agentData.name} was added successfully`,
|
||||
});
|
||||
}
|
||||
loadAgents();
|
||||
setIsDialogOpen(false);
|
||||
resetForm();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Unable to save agent",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAgent = async () => {
|
||||
if (!agentToDelete) return;
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await deleteAgent(agentToDelete.id);
|
||||
toast({
|
||||
title: "Agent deleted",
|
||||
description: "The agent was deleted successfully",
|
||||
});
|
||||
loadAgents();
|
||||
setAgentToDelete(null);
|
||||
setIsDeleteAgentDialogOpen(false);
|
||||
} catch {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Unable to delete agent",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditAgent = (agent: Agent) => {
|
||||
setEditingAgent(agent);
|
||||
setNewAgent({ ...agent });
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleMoveAgent = async (targetFolderId: string | null) => {
|
||||
if (!agentToMove) return;
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await assignAgentToFolder(agentToMove.id, targetFolderId, clientId);
|
||||
toast({
|
||||
title: "Agent moved",
|
||||
description: targetFolderId
|
||||
? `Agent moved to folder successfully`
|
||||
: "Agent removed from folder successfully",
|
||||
});
|
||||
setIsMovingDialogOpen(false);
|
||||
loadAgents();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Unable to move agent",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setAgentToMove(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddFolder = async (folderData: {
|
||||
name: string;
|
||||
description: string;
|
||||
}) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
if (editingFolder) {
|
||||
await updateFolder(editingFolder.id, folderData, clientId);
|
||||
toast({
|
||||
title: "Folder updated",
|
||||
description: `${folderData.name} was updated successfully`,
|
||||
});
|
||||
} else {
|
||||
await createFolder({
|
||||
...folderData,
|
||||
client_id: clientId,
|
||||
});
|
||||
toast({
|
||||
title: "Folder created",
|
||||
description: `${folderData.name} was created successfully`,
|
||||
});
|
||||
}
|
||||
loadFolders();
|
||||
setIsFolderDialogOpen(false);
|
||||
setEditingFolder(null);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Unable to save folder",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteFolder = async () => {
|
||||
if (!folderToDelete) return;
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await deleteFolder(folderToDelete.id, clientId);
|
||||
toast({
|
||||
title: "Folder deleted",
|
||||
description: "The folder was deleted successfully",
|
||||
});
|
||||
loadFolders();
|
||||
if (selectedFolderId === folderToDelete.id) {
|
||||
setSelectedFolderId(null);
|
||||
}
|
||||
setFolderToDelete(null);
|
||||
setIsDeleteFolderDialogOpen(false);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Unable to delete folder",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShareAgent = async (agent: Agent) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setAgentToShare(agent);
|
||||
const response = await shareAgent(agent.id, clientId);
|
||||
|
||||
if (response.data && response.data.api_key) {
|
||||
setSharedApiKey(response.data.api_key);
|
||||
setIsShareDialogOpen(true);
|
||||
|
||||
toast({
|
||||
title: "Agent shared",
|
||||
description: "API key generated successfully",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Unable to share agent",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setNewAgent({
|
||||
client_id: clientId || "",
|
||||
name: "",
|
||||
description: "",
|
||||
type: "llm",
|
||||
model: "openai/gpt-4.1-nano",
|
||||
instruction: "",
|
||||
api_key_id: "",
|
||||
config: {
|
||||
tools: [],
|
||||
mcp_servers: [],
|
||||
custom_mcp_servers: [],
|
||||
custom_tools: {
|
||||
http_tools: [],
|
||||
},
|
||||
sub_agents: [],
|
||||
agent_tools: [],
|
||||
},
|
||||
});
|
||||
setEditingAgent(null);
|
||||
};
|
||||
|
||||
// Function to export all agents as JSON
|
||||
const handleExportAllAgents = () => {
|
||||
try {
|
||||
// Create file name with current date
|
||||
const date = new Date();
|
||||
const formattedDate = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
|
||||
const filename = `agents-export-${formattedDate}`;
|
||||
|
||||
// Use the utility function to export
|
||||
// Pass agents both as the data and as allAgents parameter to properly resolve references
|
||||
const result = exportAsJson({ agents: filteredAgents }, filename, true, agents);
|
||||
|
||||
if (result) {
|
||||
toast({
|
||||
title: "Export complete",
|
||||
description: `${filteredAgents.length} agent(s) exported to JSON`,
|
||||
});
|
||||
} else {
|
||||
throw new Error("Export failed");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error exporting agents:", error);
|
||||
|
||||
toast({
|
||||
title: "Export failed",
|
||||
description: "There was an error exporting the agents",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportAgentJSON = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file || !clientId) return;
|
||||
|
||||
try {
|
||||
setIsImporting(true);
|
||||
|
||||
await importAgentFromJson(file, clientId);
|
||||
|
||||
toast({
|
||||
title: "Import successful",
|
||||
description: "Agent was imported successfully",
|
||||
});
|
||||
|
||||
// Refresh the agent list
|
||||
loadAgents();
|
||||
|
||||
// Reset file input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error importing agent:", error);
|
||||
toast({
|
||||
title: "Import failed",
|
||||
description: "There was an error importing the agent",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6 bg-[#121212] min-h-screen flex relative">
|
||||
<AgentSidebar
|
||||
visible={isSidebarVisible}
|
||||
folders={folders}
|
||||
selectedFolderId={selectedFolderId}
|
||||
onSelectFolder={setSelectedFolderId}
|
||||
onAddFolder={() => {
|
||||
setEditingFolder(null);
|
||||
setIsFolderDialogOpen(true);
|
||||
}}
|
||||
onEditFolder={(folder) => {
|
||||
setEditingFolder(folder as AgentFolder);
|
||||
setIsFolderDialogOpen(true);
|
||||
}}
|
||||
onDeleteFolder={(folder) => {
|
||||
setFolderToDelete(folder as AgentFolder);
|
||||
setIsDeleteFolderDialogOpen(true);
|
||||
}}
|
||||
onClose={() => setIsSidebarVisible(!isSidebarVisible)}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`w-full transition-all duration-300 ease-in-out ${
|
||||
isSidebarVisible ? "pl-64" : "pl-0"
|
||||
}`}
|
||||
>
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
{!isSidebarVisible && (
|
||||
<button
|
||||
onClick={() => setIsSidebarVisible(true)}
|
||||
className="mr-2 bg-[#222] p-2 rounded-md text-emerald-400 hover:bg-[#333] hover:text-emerald-400 shadow-md transition-all"
|
||||
aria-label="Show folders"
|
||||
>
|
||||
<Folder className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
<h1 className="text-3xl font-bold text-white flex items-center ml-2">
|
||||
{selectedFolderId
|
||||
? folders.find((f) => f.id === selectedFolderId)?.name
|
||||
: "Agents"}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<SearchInput
|
||||
value={searchTerm}
|
||||
onChange={setSearchTerm}
|
||||
placeholder="Search agents..."
|
||||
selectedAgentType={selectedAgentType}
|
||||
onAgentTypeChange={setSelectedAgentType}
|
||||
agentTypes={agentTypes}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={() => setIsApiKeysDialogOpen(true)}
|
||||
className="bg-[#222] text-white hover:bg-[#333] border border-[#444]"
|
||||
>
|
||||
<Key className="mr-2 h-4 w-4 text-emerald-400" />
|
||||
API Keys
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleExportAllAgents}
|
||||
className="bg-[#222] text-white hover:bg-[#333] border border-[#444]"
|
||||
title="Export all agents as JSON"
|
||||
>
|
||||
<Download className="mr-2 h-4 w-4 text-purple-400" />
|
||||
Export All
|
||||
</Button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="bg-emerald-400 text-black hover:bg-[#00cc7d]">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Agent
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="bg-zinc-900 border-zinc-700">
|
||||
<DropdownMenuItem
|
||||
className="text-white hover:bg-zinc-800 cursor-pointer"
|
||||
onClick={() => {
|
||||
resetForm();
|
||||
setIsDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2 text-emerald-400" />
|
||||
New Agent
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-white hover:bg-zinc-800 cursor-pointer"
|
||||
onClick={() => setIsImportDialogOpen(true)}
|
||||
>
|
||||
<Upload className="h-4 w-4 mr-2 text-indigo-400" />
|
||||
Import Agent JSON
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-[60vh]">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-emerald-400"></div>
|
||||
</div>
|
||||
) : filteredAgents.length > 0 ? (
|
||||
<AgentList
|
||||
agents={filteredAgents}
|
||||
isLoading={isLoading}
|
||||
searchTerm={searchTerm}
|
||||
selectedFolderId={selectedFolderId}
|
||||
availableMCPs={availableMCPs}
|
||||
getApiKeyNameById={(id) =>
|
||||
apiKeys.find((k) => k.id === id)?.name || null
|
||||
}
|
||||
getAgentNameById={(id) =>
|
||||
agents.find((a) => a.id === id)?.name || id
|
||||
}
|
||||
onEdit={handleEditAgent}
|
||||
onDelete={(agent) => {
|
||||
setAgentToDelete(agent);
|
||||
setIsDeleteAgentDialogOpen(true);
|
||||
}}
|
||||
onMove={(agent) => {
|
||||
setAgentToMove(agent);
|
||||
setIsMovingDialogOpen(true);
|
||||
}}
|
||||
onShare={handleShareAgent}
|
||||
onClearSearch={() => {
|
||||
setSearchTerm("");
|
||||
setSelectedAgentType(null);
|
||||
}}
|
||||
onCreateAgent={() => {
|
||||
resetForm();
|
||||
setIsDialogOpen(true);
|
||||
}}
|
||||
onWorkflow={(agentId) => {
|
||||
router.push(`/agents/workflows?agentId=${agentId}`);
|
||||
}}
|
||||
apiKeys={apiKeys}
|
||||
folders={folders}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
type={
|
||||
searchTerm || selectedAgentType
|
||||
? "search-no-results"
|
||||
: selectedFolderId
|
||||
? "empty-folder"
|
||||
: "no-agents"
|
||||
}
|
||||
searchTerm={searchTerm}
|
||||
onAction={() => {
|
||||
searchTerm || selectedAgentType
|
||||
? (setSearchTerm(""), setSelectedAgentType(null))
|
||||
: (resetForm(), setIsDialogOpen(true));
|
||||
}}
|
||||
actionLabel={searchTerm || selectedAgentType ? "Clear filters" : "Create Agent"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AgentForm
|
||||
open={isDialogOpen}
|
||||
onOpenChange={setIsDialogOpen}
|
||||
initialValues={newAgent}
|
||||
apiKeys={apiKeys}
|
||||
availableModels={availableModels}
|
||||
availableMCPs={availableMCPs}
|
||||
agents={agents}
|
||||
onOpenApiKeysDialog={() => setIsApiKeysDialogOpen(true)}
|
||||
onOpenMCPDialog={() => setIsMCPDialogOpen(true)}
|
||||
onOpenCustomMCPDialog={() => setIsCustomMCPDialogOpen(true)}
|
||||
onSave={handleAddAgent}
|
||||
getAgentNameById={(id) => agents.find((a) => a.id === id)?.name || id}
|
||||
clientId={clientId}
|
||||
/>
|
||||
|
||||
<FolderDialog
|
||||
open={isFolderDialogOpen}
|
||||
onOpenChange={setIsFolderDialogOpen}
|
||||
editingFolder={editingFolder}
|
||||
onSave={handleAddFolder}
|
||||
/>
|
||||
|
||||
<MoveAgentDialog
|
||||
open={isMovingDialogOpen}
|
||||
onOpenChange={setIsMovingDialogOpen}
|
||||
agent={agentToMove}
|
||||
folders={folders}
|
||||
onMove={handleMoveAgent}
|
||||
/>
|
||||
|
||||
<ConfirmationDialog
|
||||
open={isDeleteAgentDialogOpen}
|
||||
onOpenChange={setIsDeleteAgentDialogOpen}
|
||||
title="Confirm deletion"
|
||||
description={`Are you sure you want to delete the agent "${agentToDelete?.name}"? This action cannot be undone.`}
|
||||
onConfirm={handleDeleteAgent}
|
||||
/>
|
||||
|
||||
<ConfirmationDialog
|
||||
open={isDeleteFolderDialogOpen}
|
||||
onOpenChange={setIsDeleteFolderDialogOpen}
|
||||
title="Confirm deletion"
|
||||
description={`Are you sure you want to delete the folder "${folderToDelete?.name}"? This action cannot be undone.`}
|
||||
confirmText="Delete"
|
||||
confirmVariant="destructive"
|
||||
onConfirm={handleDeleteFolder}
|
||||
/>
|
||||
|
||||
<ApiKeysDialog
|
||||
open={isApiKeysDialogOpen}
|
||||
onOpenChange={setIsApiKeysDialogOpen}
|
||||
apiKeys={apiKeys}
|
||||
isLoading={isLoading}
|
||||
onAddApiKey={async (keyData) => {
|
||||
await createApiKey({ ...keyData, client_id: clientId });
|
||||
loadApiKeys();
|
||||
}}
|
||||
onUpdateApiKey={async (id, keyData) => {
|
||||
await updateApiKey(id, keyData, clientId);
|
||||
loadApiKeys();
|
||||
}}
|
||||
onDeleteApiKey={async (id) => {
|
||||
await deleteApiKey(id, clientId);
|
||||
loadApiKeys();
|
||||
}}
|
||||
/>
|
||||
|
||||
<ShareAgentDialog
|
||||
open={isShareDialogOpen}
|
||||
onOpenChange={setIsShareDialogOpen}
|
||||
agent={agentToShare || ({} as Agent)}
|
||||
apiKey={sharedApiKey}
|
||||
/>
|
||||
|
||||
<ImportAgentDialog
|
||||
open={isImportDialogOpen}
|
||||
onOpenChange={setIsImportDialogOpen}
|
||||
onSuccess={loadAgents}
|
||||
clientId={clientId}
|
||||
folderId={selectedFolderId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
689
frontend/app/agents/workflows/Canva.tsx
Normal file
689
frontend/app/agents/workflows/Canva.tsx
Normal file
@@ -0,0 +1,689 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/Canva.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Delay node integration developed by: Victor Calazans │
|
||||
│ Creation date: May 13, 2025 |
|
||||
│ Delay implementation date: May 17, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
|
||||
import {
|
||||
Controls,
|
||||
ReactFlow,
|
||||
addEdge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
type OnConnect,
|
||||
ConnectionMode,
|
||||
ConnectionLineType,
|
||||
useReactFlow,
|
||||
ProOptions,
|
||||
applyNodeChanges,
|
||||
NodeChange,
|
||||
OnNodesChange,
|
||||
MiniMap,
|
||||
Panel,
|
||||
Background,
|
||||
} from "@xyflow/react";
|
||||
import { useDnD } from "@/contexts/DnDContext";
|
||||
|
||||
import { Edit, X, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import "./canva.css";
|
||||
|
||||
import { getHelperLines } from "./utils";
|
||||
|
||||
import { NodePanel } from "./NodePanel";
|
||||
import ContextMenu from "./ContextMenu";
|
||||
import { initialEdges, edgeTypes } from "./edges";
|
||||
import HelperLines from "./HelperLines";
|
||||
import { initialNodes, nodeTypes } from "./nodes";
|
||||
import { AgentForm } from "./nodes/components/agent/AgentForm";
|
||||
import { ConditionForm } from "./nodes/components/condition/ConditionForm";
|
||||
import { Agent, WorkflowData } from "@/types/agent";
|
||||
import { updateAgent } from "@/services/agentService";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { MessageForm } from "./nodes/components/message/MessageForm";
|
||||
import { DelayForm } from "./nodes/components/delay/DelayForm";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const proOptions: ProOptions = { account: "paid-pro", hideAttribution: true };
|
||||
|
||||
const NodeFormWrapper = ({
|
||||
selectedNode,
|
||||
editingLabel,
|
||||
setEditingLabel,
|
||||
handleUpdateNode,
|
||||
setSelectedNode,
|
||||
children,
|
||||
}: {
|
||||
selectedNode: any;
|
||||
editingLabel: boolean;
|
||||
setEditingLabel: (value: boolean) => void;
|
||||
handleUpdateNode: (node: any) => void;
|
||||
setSelectedNode: (node: any) => void;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
// Handle ESC key to close the panel
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && !editingLabel) {
|
||||
setSelectedNode(null);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [setSelectedNode, editingLabel]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-shrink-0 sticky top-0 z-20 bg-neutral-800 shadow-md border-b border-neutral-700">
|
||||
<div className="p-4 text-center relative">
|
||||
<button
|
||||
className="absolute right-2 top-2 text-neutral-200 hover:text-white p-1 rounded-full hover:bg-neutral-700"
|
||||
onClick={() => setSelectedNode(null)}
|
||||
aria-label="Close panel"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
{!editingLabel ? (
|
||||
<div className="flex items-center justify-center text-xl font-bold text-neutral-200">
|
||||
<span>{selectedNode.data.label}</span>
|
||||
{selectedNode.type !== "start-node" && (
|
||||
<Edit
|
||||
size={16}
|
||||
className="ml-2 cursor-pointer hover:text-indigo-300"
|
||||
onClick={() => setEditingLabel(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={selectedNode.data.label}
|
||||
className="w-full p-2 text-center text-xl font-bold bg-neutral-800 text-neutral-200 border border-neutral-600 rounded"
|
||||
onChange={(e) => {
|
||||
handleUpdateNode({
|
||||
...selectedNode,
|
||||
data: {
|
||||
...selectedNode.data,
|
||||
label: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
setEditingLabel(false);
|
||||
}
|
||||
}}
|
||||
onBlur={() => setEditingLabel(false)}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0 overflow-hidden">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Canva = forwardRef(({ agent }: { agent: Agent | null }, ref) => {
|
||||
const { toast } = useToast();
|
||||
const [nodes, setNodes] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
const { type, setPointerEvents } = useDnD();
|
||||
const [menu, setMenu] = useState<any>(null);
|
||||
const localRef = useRef<any>(null);
|
||||
const [selectedNode, setSelectedNode] = useState<any>(null);
|
||||
const [activeExecutionNodeId, setActiveExecutionNodeId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
const [editingLabel, setEditingLabel] = useState(false);
|
||||
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
const [nodePanelOpen, setNodePanelOpen] = useState(false);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getFlowData: () => ({
|
||||
nodes,
|
||||
edges,
|
||||
}),
|
||||
setHasChanges,
|
||||
setActiveExecutionNodeId,
|
||||
}));
|
||||
|
||||
// Effect to clear the active node after a timeout
|
||||
useEffect(() => {
|
||||
if (activeExecutionNodeId) {
|
||||
const timer = setTimeout(() => {
|
||||
setActiveExecutionNodeId(null);
|
||||
}, 5000); // Increase to 5 seconds to give more time to visualize
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [activeExecutionNodeId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
agent?.config?.workflow &&
|
||||
agent.config.workflow.nodes.length > 0 &&
|
||||
agent.config.workflow.edges.length > 0
|
||||
) {
|
||||
setNodes(
|
||||
(agent.config.workflow.nodes as typeof initialNodes) || initialNodes
|
||||
);
|
||||
setEdges(
|
||||
(agent.config.workflow.edges as typeof initialEdges) || initialEdges
|
||||
);
|
||||
} else {
|
||||
setNodes(initialNodes);
|
||||
setEdges(initialEdges);
|
||||
}
|
||||
}, [agent, setNodes, setEdges]);
|
||||
|
||||
// Update nodes when the active node changes to add visual class
|
||||
useEffect(() => {
|
||||
if (nodes.length > 0) {
|
||||
setNodes((nds: any) =>
|
||||
nds.map((node: any) => {
|
||||
if (node.id === activeExecutionNodeId) {
|
||||
// Add a class to highlight the active node
|
||||
return {
|
||||
...node,
|
||||
className: "active-execution-node",
|
||||
data: {
|
||||
...node.data,
|
||||
isExecuting: true,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// Remove the highlight class
|
||||
const { isExecuting, ...restData } = node.data || {};
|
||||
return {
|
||||
...node,
|
||||
className: "",
|
||||
data: restData,
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [activeExecutionNodeId, setNodes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (agent?.config?.workflow) {
|
||||
const initialNodes = agent.config.workflow.nodes || [];
|
||||
const initialEdges = agent.config.workflow.edges || [];
|
||||
|
||||
if (
|
||||
JSON.stringify(nodes) !== JSON.stringify(initialNodes) ||
|
||||
JSON.stringify(edges) !== JSON.stringify(initialEdges)
|
||||
) {
|
||||
setHasChanges(true);
|
||||
} else {
|
||||
setHasChanges(false);
|
||||
}
|
||||
}
|
||||
}, [nodes, edges, agent]);
|
||||
|
||||
const [helperLineHorizontal, setHelperLineHorizontal] = useState<
|
||||
number | undefined
|
||||
>(undefined);
|
||||
const [helperLineVertical, setHelperLineVertical] = useState<
|
||||
number | undefined
|
||||
>(undefined);
|
||||
|
||||
const onConnect: OnConnect = useCallback(
|
||||
(connection) => {
|
||||
setEdges((currentEdges) => {
|
||||
if (connection.source === connection.target) {
|
||||
return currentEdges;
|
||||
}
|
||||
|
||||
return addEdge(connection, currentEdges);
|
||||
});
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const onConnectEnd = useCallback(
|
||||
(_event: any, connectionState: any) => {
|
||||
setPointerEvents("none");
|
||||
|
||||
if (connectionState.fromHandle?.type === "target") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connectionState.isValid) {
|
||||
// Since we're using NodePanel now, we don't need to do anything here
|
||||
// The panel will handle node creation through drag and drop
|
||||
}
|
||||
},
|
||||
[setPointerEvents]
|
||||
);
|
||||
|
||||
const onConnectStart = useCallback(() => {
|
||||
setPointerEvents("auto");
|
||||
}, [setPointerEvents]);
|
||||
|
||||
const customApplyNodeChanges = useCallback(
|
||||
(changes: NodeChange[], nodes: any): any => {
|
||||
// reset the helper lines (clear existing lines, if any)
|
||||
setHelperLineHorizontal(undefined);
|
||||
setHelperLineVertical(undefined);
|
||||
|
||||
// this will be true if it's a single node being dragged
|
||||
// inside we calculate the helper lines and snap position for the position where the node is being moved to
|
||||
if (
|
||||
changes.length === 1 &&
|
||||
changes[0].type === "position" &&
|
||||
changes[0].dragging &&
|
||||
changes[0].position
|
||||
) {
|
||||
const helperLines = getHelperLines(changes[0], nodes);
|
||||
|
||||
// if we have a helper line, we snap the node to the helper line position
|
||||
// this is being done by manipulating the node position inside the change object
|
||||
changes[0].position.x =
|
||||
helperLines.snapPosition.x ?? changes[0].position.x;
|
||||
changes[0].position.y =
|
||||
helperLines.snapPosition.y ?? changes[0].position.y;
|
||||
|
||||
// if helper lines are returned, we set them so that they can be displayed
|
||||
setHelperLineHorizontal(helperLines.horizontal);
|
||||
setHelperLineVertical(helperLines.vertical);
|
||||
}
|
||||
|
||||
return applyNodeChanges(changes, nodes);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const onNodesChange: OnNodesChange = useCallback(
|
||||
(changes) => {
|
||||
setNodes((nodes) => customApplyNodeChanges(changes, nodes));
|
||||
},
|
||||
[setNodes, customApplyNodeChanges]
|
||||
);
|
||||
|
||||
const getLabelFromNode = (type: string) => {
|
||||
const order = nodes.length;
|
||||
|
||||
switch (type) {
|
||||
case "start-node":
|
||||
return "Start";
|
||||
case "agent-node":
|
||||
return `Agent #${order}`;
|
||||
case "condition-node":
|
||||
return `Condition #${order}`;
|
||||
case "message-node":
|
||||
return `Message #${order}`;
|
||||
case "delay-node":
|
||||
return `Delay #${order}`;
|
||||
default:
|
||||
return "Node";
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddNode = useCallback(
|
||||
(type: any, node: any) => {
|
||||
const newNode: any = {
|
||||
id: String(Date.now()),
|
||||
type,
|
||||
position: node.position,
|
||||
data: {
|
||||
label: getLabelFromNode(type),
|
||||
},
|
||||
};
|
||||
|
||||
setNodes((nodes) => [...nodes, newNode]);
|
||||
|
||||
if (node.targetId) {
|
||||
const newEdge: any = {
|
||||
source: node.targetId,
|
||||
sourceHandle: node.handleId,
|
||||
target: newNode.id,
|
||||
type: "default",
|
||||
};
|
||||
|
||||
const newsEdges: any = [...edges, newEdge];
|
||||
|
||||
setEdges(newsEdges);
|
||||
}
|
||||
},
|
||||
[nodes, setNodes, edges, setEdges]
|
||||
);
|
||||
|
||||
const handleUpdateNode = useCallback(
|
||||
(node: any) => {
|
||||
setNodes((nodes) => {
|
||||
const index = nodes.findIndex((n) => n.id === node.id);
|
||||
if (index !== -1) {
|
||||
nodes[index] = node;
|
||||
}
|
||||
return [...nodes];
|
||||
});
|
||||
|
||||
if (selectedNode && selectedNode.id === node.id) {
|
||||
setSelectedNode(node);
|
||||
}
|
||||
},
|
||||
[setNodes, selectedNode]
|
||||
);
|
||||
|
||||
const handleDeleteEdge = useCallback(
|
||||
(id: any) => {
|
||||
setEdges((edges) => {
|
||||
const left = edges.filter((edge: any) => edge.id !== id);
|
||||
return left;
|
||||
});
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const onDragOver = useCallback((event: any) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
}, []);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(event: any) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
const newNode: any = {
|
||||
id: String(Date.now()),
|
||||
type,
|
||||
position,
|
||||
data: {
|
||||
label: getLabelFromNode(type),
|
||||
},
|
||||
};
|
||||
|
||||
setNodes((nodes) => [...nodes, newNode]);
|
||||
},
|
||||
[screenToFlowPosition, setNodes, type, getLabelFromNode]
|
||||
);
|
||||
|
||||
const onNodeContextMenu = useCallback(
|
||||
(event: any, node: any) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (node.id === "start-node") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!localRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const paneBounds = localRef.current.getBoundingClientRect();
|
||||
|
||||
const x = event.clientX - paneBounds.left;
|
||||
const y = event.clientY - paneBounds.top;
|
||||
|
||||
const menuWidth = 200;
|
||||
const menuHeight = 200;
|
||||
|
||||
const left = x + menuWidth > paneBounds.width ? undefined : x;
|
||||
const top = y + menuHeight > paneBounds.height ? undefined : y;
|
||||
const right =
|
||||
x + menuWidth > paneBounds.width ? paneBounds.width - x : undefined;
|
||||
const bottom =
|
||||
y + menuHeight > paneBounds.height ? paneBounds.height - y : undefined;
|
||||
|
||||
setMenu({
|
||||
id: node.id,
|
||||
left,
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
});
|
||||
},
|
||||
[setMenu]
|
||||
);
|
||||
|
||||
const onNodeClick = useCallback((event: any, node: any) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (node.type === "start-node") {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedNode(node);
|
||||
}, []);
|
||||
|
||||
const onPaneClick = useCallback(() => {
|
||||
setMenu(null);
|
||||
setSelectedNode(null);
|
||||
setNodePanelOpen(false);
|
||||
}, [setMenu, setSelectedNode]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full bg-[#121212]">
|
||||
<div
|
||||
style={{ position: "relative", height: "100%", width: "100%" }}
|
||||
ref={localRef}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodesChange={onNodesChange}
|
||||
edges={edges}
|
||||
edgeTypes={edgeTypes}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onConnectStart={onConnectStart}
|
||||
onConnectEnd={onConnectEnd}
|
||||
connectionMode={ConnectionMode.Strict}
|
||||
connectionLineType={ConnectionLineType.Bezier}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
onPaneClick={onPaneClick}
|
||||
onNodeClick={onNodeClick}
|
||||
onNodeContextMenu={onNodeContextMenu}
|
||||
colorMode="dark"
|
||||
minZoom={0.1}
|
||||
maxZoom={10}
|
||||
fitView={false}
|
||||
defaultViewport={{
|
||||
x: 0,
|
||||
y: 0,
|
||||
zoom: 1,
|
||||
}}
|
||||
elevateEdgesOnSelect
|
||||
elevateNodesOnSelect
|
||||
proOptions={proOptions}
|
||||
connectionLineStyle={{
|
||||
stroke: "gray",
|
||||
strokeWidth: 2,
|
||||
strokeDashoffset: 5,
|
||||
strokeDasharray: 5,
|
||||
}}
|
||||
defaultEdgeOptions={{
|
||||
type: "default",
|
||||
style: {
|
||||
strokeWidth: 3,
|
||||
},
|
||||
data: {
|
||||
handleDeleteEdge,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background color="#334155" gap={24} size={1.5} />
|
||||
<MiniMap
|
||||
className="bg-neutral-800/80 border border-neutral-700 rounded-lg shadow-lg"
|
||||
nodeColor={(node) => {
|
||||
switch (node.type) {
|
||||
case "start-node":
|
||||
return "#10b981";
|
||||
case "agent-node":
|
||||
return "#3b82f6";
|
||||
case "message-node":
|
||||
return "#f97316";
|
||||
case "condition-node":
|
||||
return "#3b82f6";
|
||||
case "delay-node":
|
||||
return "#eab308";
|
||||
default:
|
||||
return "#64748b";
|
||||
}
|
||||
}}
|
||||
maskColor="rgba(15, 23, 42, 0.6)"
|
||||
/>
|
||||
|
||||
<Controls
|
||||
showInteractive={true}
|
||||
showFitView={true}
|
||||
orientation="vertical"
|
||||
position="bottom-left"
|
||||
/>
|
||||
<HelperLines
|
||||
horizontal={helperLineHorizontal}
|
||||
vertical={helperLineVertical}
|
||||
/>
|
||||
{menu && <ContextMenu onClick={onPaneClick} {...menu} />}
|
||||
|
||||
{nodePanelOpen ? (
|
||||
<Panel position="top-right">
|
||||
<div className="flex items-start">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setNodePanelOpen(false)}
|
||||
className="mr-2 h-8 w-8 rounded-full bg-slate-800 border-slate-700 text-slate-400 hover:text-white hover:bg-slate-700"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
<NodePanel />
|
||||
</div>
|
||||
</Panel>
|
||||
) : (
|
||||
<Panel position="top-right">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setNodePanelOpen(true)}
|
||||
className="h-8 w-8 rounded-full bg-slate-800 border-slate-700 text-slate-400 hover:text-white hover:bg-slate-700"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
</Panel>
|
||||
)}
|
||||
</ReactFlow>
|
||||
|
||||
{/* Overlay when form is open on smaller screens */}
|
||||
{selectedNode && (
|
||||
<div
|
||||
className="md:hidden fixed inset-0 bg-black bg-opacity-50 z-[5] transition-opacity duration-300"
|
||||
onClick={() => setSelectedNode(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="absolute left-0 top-0 z-10 h-full w-[350px] bg-neutral-900 shadow-lg transition-all duration-300 ease-in-out border-r border-neutral-700 flex flex-col"
|
||||
style={{
|
||||
transform: selectedNode ? "translateX(0)" : "translateX(-100%)",
|
||||
opacity: selectedNode ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
{selectedNode ? (
|
||||
<NodeFormWrapper
|
||||
selectedNode={selectedNode}
|
||||
editingLabel={editingLabel}
|
||||
setEditingLabel={setEditingLabel}
|
||||
handleUpdateNode={handleUpdateNode}
|
||||
setSelectedNode={setSelectedNode}
|
||||
>
|
||||
{selectedNode.type === "agent-node" && (
|
||||
<AgentForm
|
||||
selectedNode={selectedNode}
|
||||
handleUpdateNode={handleUpdateNode}
|
||||
setEdges={setEdges}
|
||||
setIsOpen={() => {}}
|
||||
setSelectedNode={setSelectedNode}
|
||||
/>
|
||||
)}
|
||||
{selectedNode.type === "condition-node" && (
|
||||
<ConditionForm
|
||||
selectedNode={selectedNode}
|
||||
handleUpdateNode={handleUpdateNode}
|
||||
setEdges={setEdges}
|
||||
setIsOpen={() => {}}
|
||||
setSelectedNode={setSelectedNode}
|
||||
/>
|
||||
)}
|
||||
{selectedNode.type === "message-node" && (
|
||||
<MessageForm
|
||||
selectedNode={selectedNode}
|
||||
handleUpdateNode={handleUpdateNode}
|
||||
setEdges={setEdges}
|
||||
setIsOpen={() => {}}
|
||||
setSelectedNode={setSelectedNode}
|
||||
/>
|
||||
)}
|
||||
{selectedNode.type === "delay-node" && (
|
||||
<DelayForm
|
||||
selectedNode={selectedNode}
|
||||
handleUpdateNode={handleUpdateNode}
|
||||
setEdges={setEdges}
|
||||
setIsOpen={() => {}}
|
||||
setSelectedNode={setSelectedNode}
|
||||
/>
|
||||
)}
|
||||
</NodeFormWrapper>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Canva.displayName = "Canva";
|
||||
|
||||
export default Canva;
|
||||
118
frontend/app/agents/workflows/ContextMenu.tsx
Normal file
118
frontend/app/agents/workflows/ContextMenu.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/ContextMenu.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
import { useReactFlow, Node, Edge } from "@xyflow/react";
|
||||
import { Copy, Trash2 } from "lucide-react";
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
interface ContextMenuProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
id: string;
|
||||
top?: number | string;
|
||||
left?: number | string;
|
||||
right?: number | string;
|
||||
bottom?: number | string;
|
||||
}
|
||||
|
||||
export default function ContextMenu({
|
||||
id,
|
||||
top,
|
||||
left,
|
||||
right,
|
||||
bottom,
|
||||
...props
|
||||
}: ContextMenuProps) {
|
||||
const { getNode, setNodes, addNodes, setEdges } = useReactFlow();
|
||||
|
||||
const duplicateNode = useCallback(() => {
|
||||
const node = getNode(id);
|
||||
|
||||
if (!node) {
|
||||
console.error(`Node with id ${id} not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const position = {
|
||||
x: node.position.x + 50,
|
||||
y: node.position.y + 50,
|
||||
};
|
||||
|
||||
addNodes({
|
||||
...node,
|
||||
id: `${node.id}-copy`,
|
||||
position,
|
||||
selected: false,
|
||||
dragging: false,
|
||||
});
|
||||
}, [id, getNode, addNodes]);
|
||||
|
||||
const deleteNode = useCallback(() => {
|
||||
setNodes((nodes: Node[]) => nodes.filter((node) => node.id !== id));
|
||||
setEdges((edges: Edge[]) =>
|
||||
edges.filter((edge) => edge.source !== id && edge.target !== id),
|
||||
);
|
||||
}, [id, setNodes, setEdges]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: top !== undefined ? `${top}px` : undefined,
|
||||
left: left !== undefined ? `${left}px` : undefined,
|
||||
right: right !== undefined ? `${right}px` : undefined,
|
||||
bottom: bottom !== undefined ? `${bottom}px` : undefined,
|
||||
zIndex: 10,
|
||||
}}
|
||||
className="context-menu rounded-md border p-3 shadow-lg border-neutral-700 bg-neutral-800"
|
||||
{...props}
|
||||
>
|
||||
<p className="mb-2 text-sm font-semibold text-neutral-200">
|
||||
Actions
|
||||
</p>
|
||||
<button
|
||||
onClick={duplicateNode}
|
||||
className="mb-1 flex w-full flex-row items-center rounded-md px-2 py-1 text-sm hover:bg-neutral-700"
|
||||
>
|
||||
<Copy
|
||||
size={16}
|
||||
className="mr-2 flex-shrink-0 text-blue-300"
|
||||
/>
|
||||
<span className="text-neutral-300">Duplicate</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={deleteNode}
|
||||
className="flex w-full flex-row items-center rounded-md px-2 py-1 text-sm hover:bg-neutral-700"
|
||||
>
|
||||
<Trash2
|
||||
size={16}
|
||||
className="mr-2 flex-shrink-0 text-red-300"
|
||||
/>
|
||||
<span className="text-neutral-300">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
98
frontend/app/agents/workflows/HelperLines.tsx
Normal file
98
frontend/app/agents/workflows/HelperLines.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/HelperLines.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
import { ReactFlowState, useStore } from "@xyflow/react";
|
||||
import { CSSProperties, useEffect, useRef } from "react";
|
||||
|
||||
const canvasStyle: CSSProperties = {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
zIndex: 10,
|
||||
pointerEvents: "none",
|
||||
};
|
||||
|
||||
const storeSelector = (state: ReactFlowState) => ({
|
||||
width: state.width,
|
||||
height: state.height,
|
||||
transform: state.transform,
|
||||
});
|
||||
|
||||
export type HelperLinesProps = {
|
||||
horizontal?: number;
|
||||
vertical?: number;
|
||||
};
|
||||
|
||||
// a simple component to display the helper lines
|
||||
// it puts a canvas on top of the React Flow pane and draws the lines using the canvas API
|
||||
function HelperLinesRenderer({ horizontal, vertical }: HelperLinesProps) {
|
||||
const { width, height, transform } = useStore(storeSelector);
|
||||
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas?.getContext("2d");
|
||||
|
||||
if (!ctx || !canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dpi = window.devicePixelRatio;
|
||||
canvas.width = width * dpi;
|
||||
canvas.height = height * dpi;
|
||||
|
||||
ctx.scale(dpi, dpi);
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.strokeStyle = "#1d5ade";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.setLineDash([5, 5]);
|
||||
|
||||
if (typeof vertical === "number") {
|
||||
ctx.moveTo(vertical * transform[2] + transform[0], 0);
|
||||
ctx.lineTo(vertical * transform[2] + transform[0], height);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
if (typeof horizontal === "number") {
|
||||
ctx.moveTo(0, horizontal * transform[2] + transform[1]);
|
||||
ctx.lineTo(width, horizontal * transform[2] + transform[1]);
|
||||
ctx.stroke();
|
||||
}
|
||||
}, [width, height, transform, horizontal, vertical]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="react-flow__canvas"
|
||||
style={canvasStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default HelperLinesRenderer;
|
||||
273
frontend/app/agents/workflows/NodePanel.tsx
Normal file
273
frontend/app/agents/workflows/NodePanel.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
"use client";
|
||||
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
User,
|
||||
MessageSquare,
|
||||
Filter,
|
||||
Clock,
|
||||
Plus,
|
||||
MenuSquare,
|
||||
Layers,
|
||||
MoveRight,
|
||||
} from "lucide-react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useDnD } from "@/contexts/DnDContext";
|
||||
|
||||
export function NodePanel() {
|
||||
const [activeTab, setActiveTab] = useState("content");
|
||||
const { setType } = useDnD();
|
||||
|
||||
const nodeTypes = {
|
||||
content: [
|
||||
{
|
||||
id: "agent-node",
|
||||
name: "Agent",
|
||||
icon: User,
|
||||
color: "text-blue-400",
|
||||
bgColor: "bg-blue-950/40",
|
||||
borderColor: "border-blue-500/30",
|
||||
hoverColor: "group-hover:bg-blue-900/50",
|
||||
glowColor: "group-hover:shadow-blue-500/20",
|
||||
description: "Add an AI agent to process messages and execute tasks",
|
||||
},
|
||||
{
|
||||
id: "message-node",
|
||||
name: "Message",
|
||||
icon: MessageSquare,
|
||||
color: "text-orange-400",
|
||||
bgColor: "bg-orange-950/40",
|
||||
borderColor: "border-orange-500/30",
|
||||
hoverColor: "group-hover:bg-orange-900/50",
|
||||
glowColor: "group-hover:shadow-orange-500/20",
|
||||
description: "Send a message to users or other nodes in the workflow",
|
||||
},
|
||||
],
|
||||
logic: [
|
||||
{
|
||||
id: "condition-node",
|
||||
name: "Condition",
|
||||
icon: Filter,
|
||||
color: "text-purple-400",
|
||||
bgColor: "bg-purple-950/40",
|
||||
borderColor: "border-purple-500/30",
|
||||
hoverColor: "group-hover:bg-purple-900/50",
|
||||
glowColor: "group-hover:shadow-purple-500/20",
|
||||
description:
|
||||
"Create a decision point with multiple outcomes based on conditions",
|
||||
},
|
||||
{
|
||||
id: "delay-node",
|
||||
name: "Delay",
|
||||
icon: Clock,
|
||||
color: "text-yellow-400",
|
||||
bgColor: "bg-yellow-950/40",
|
||||
borderColor: "border-yellow-500/30",
|
||||
hoverColor: "group-hover:bg-yellow-900/50",
|
||||
glowColor: "group-hover:shadow-yellow-500/20",
|
||||
description: "Add a time delay between workflow operations",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const onDragStart = (event: React.DragEvent, nodeType: string) => {
|
||||
event.dataTransfer.setData("application/reactflow", nodeType);
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
setType(nodeType);
|
||||
};
|
||||
|
||||
const handleNodeAdd = (nodeType: string) => {
|
||||
setType(nodeType);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-slate-900/70 backdrop-blur-md border border-slate-700/50 rounded-xl shadow-xl w-[320px] transition-all duration-300 ease-in-out overflow-hidden">
|
||||
<div className="px-4 pt-4 pb-2 border-b border-slate-700/50">
|
||||
<div className="flex items-center gap-2 text-slate-200">
|
||||
<Layers className="h-5 w-5 text-indigo-400" />
|
||||
<h3 className="font-medium">Workflow Nodes</h3>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
Drag nodes to the canvas or click to add
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<div className="px-4 pt-3">
|
||||
<TabsList className="w-full bg-slate-800/50 grid grid-cols-2 p-1 rounded-lg">
|
||||
<TabsTrigger
|
||||
value="content"
|
||||
className={cn(
|
||||
"rounded-md text-xs font-medium transition-all",
|
||||
"data-[state=active]:bg-gradient-to-br data-[state=active]:from-blue-900/30 data-[state=active]:to-indigo-900/30",
|
||||
"data-[state=active]:text-blue-300 data-[state=active]:shadow-sm",
|
||||
"data-[state=inactive]:text-slate-400"
|
||||
)}
|
||||
>
|
||||
<MenuSquare className="h-3.5 w-3.5 mr-1.5" />
|
||||
Content
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="logic"
|
||||
className={cn(
|
||||
"rounded-md text-xs font-medium transition-all",
|
||||
"data-[state=active]:bg-gradient-to-br data-[state=active]:from-yellow-900/30 data-[state=active]:to-orange-900/30",
|
||||
"data-[state=active]:text-yellow-300 data-[state=active]:shadow-sm",
|
||||
"data-[state=inactive]:text-slate-400"
|
||||
)}
|
||||
>
|
||||
<Filter className="h-3.5 w-3.5 mr-1.5" />
|
||||
Logic
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="content" className="p-3 space-y-2 mt-0">
|
||||
{nodeTypes.content.map((node) => (
|
||||
<TooltipProvider key={node.id} delayDuration={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
draggable
|
||||
onDragStart={(event) => onDragStart(event, node.id)}
|
||||
className={cn(
|
||||
"group flex items-center gap-3 p-3.5 border rounded-lg cursor-grab transition-all duration-300",
|
||||
"backdrop-blur-sm hover:shadow-lg",
|
||||
node.borderColor,
|
||||
node.bgColor,
|
||||
node.glowColor
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center w-9 h-9 rounded-lg transition-all duration-300",
|
||||
"bg-slate-800/80 group-hover:scale-105",
|
||||
node.hoverColor
|
||||
)}
|
||||
>
|
||||
<node.icon className={cn("h-5 w-5", node.color)} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span
|
||||
className={cn("font-medium block text-sm", node.color)}
|
||||
>
|
||||
{node.name}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400 truncate block">
|
||||
{node.description}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => handleNodeAdd(node.id)}
|
||||
className={cn(
|
||||
"flex items-center justify-center h-7 w-7 rounded-md bg-slate-800/60 text-slate-400",
|
||||
"hover:bg-gradient-to-r hover:text-white transition-all",
|
||||
node.id === "agent-node"
|
||||
? "hover:from-blue-800 hover:to-blue-600"
|
||||
: "hover:from-orange-800 hover:to-orange-600"
|
||||
)}
|
||||
>
|
||||
<Plus size={16} />
|
||||
</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
className="bg-slate-900 border-slate-700 text-slate-200"
|
||||
>
|
||||
<div className="p-1 max-w-[200px]">
|
||||
<p className="font-medium text-sm">{node.name} Node</p>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
{node.description}
|
||||
</p>
|
||||
<div className="flex items-center mt-2 pt-2 border-t border-slate-700/50 text-xs text-slate-400">
|
||||
<MoveRight className="h-3 w-3 mr-1.5" />
|
||||
<span>Drag to canvas or click + to add</span>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
))}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="logic" className="p-3 space-y-2 mt-0">
|
||||
{nodeTypes.logic.map((node) => (
|
||||
<TooltipProvider key={node.id} delayDuration={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
draggable
|
||||
onDragStart={(event) => onDragStart(event, node.id)}
|
||||
className={cn(
|
||||
"group flex items-center gap-3 p-3.5 border rounded-lg cursor-grab transition-all duration-300",
|
||||
"backdrop-blur-sm hover:shadow-lg",
|
||||
node.borderColor,
|
||||
node.bgColor,
|
||||
node.glowColor
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center w-9 h-9 rounded-lg transition-all duration-300",
|
||||
"bg-slate-800/80 group-hover:scale-105",
|
||||
node.hoverColor
|
||||
)}
|
||||
>
|
||||
<node.icon className={cn("h-5 w-5", node.color)} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span
|
||||
className={cn("font-medium block text-sm", node.color)}
|
||||
>
|
||||
{node.name}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400 truncate block">
|
||||
{node.description}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => handleNodeAdd(node.id)}
|
||||
className={cn(
|
||||
"flex items-center justify-center h-7 w-7 rounded-md bg-slate-800/60 text-slate-400",
|
||||
"hover:bg-gradient-to-r hover:text-white transition-all",
|
||||
node.id === "condition-node"
|
||||
? "hover:from-purple-800 hover:to-purple-600"
|
||||
: "hover:from-yellow-800 hover:to-yellow-600"
|
||||
)}
|
||||
>
|
||||
<Plus size={16} />
|
||||
</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
className="bg-slate-900 border-slate-700 text-slate-200"
|
||||
>
|
||||
<div className="p-1 max-w-[200px]">
|
||||
<p className="font-medium text-sm">{node.name} Node</p>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
{node.description}
|
||||
</p>
|
||||
<div className="flex items-center mt-2 pt-2 border-t border-slate-700/50 text-xs text-slate-400">
|
||||
<MoveRight className="h-3 w-3 mr-1.5" />
|
||||
<span>Drag to canvas or click + to add</span>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
))}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
188
frontend/app/agents/workflows/canva.css
Normal file
188
frontend/app/agents/workflows/canva.css
Normal file
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/canva.css │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
.react-flow.dark {
|
||||
--xy-background-color-default: transparent;
|
||||
}
|
||||
|
||||
.react-flow__panel {
|
||||
box-shadow: none;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.react-flow__controls {
|
||||
background-color: rgba(30, 30, 30, 0.8);
|
||||
backdrop-filter: blur(4px);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(70, 70, 70, 0.5);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.react-flow__controls-button {
|
||||
padding: 6px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 4px 0;
|
||||
border: none !important;
|
||||
background-color: rgba(40, 40, 40, 0.8) !important;
|
||||
color: #a0a0a0 !important;
|
||||
border-radius: 6px !important;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.react-flow__controls-button:hover {
|
||||
background-color: rgba(60, 60, 60, 0.9) !important;
|
||||
color: #ffffff !important;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.react-flow__controls-button svg {
|
||||
fill: currentColor;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.react-flow__controls-button[data-action="zoom-in"] {
|
||||
border-bottom: 1px solid rgba(70, 70, 70, 0.5) !important;
|
||||
}
|
||||
|
||||
.react-flow__controls-button[data-action="zoom-out"] {
|
||||
border-bottom: 1px solid rgba(70, 70, 70, 0.5) !important;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for all browsers while maintaining scroll functionality */
|
||||
.scrollbar-hide {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari and Opera */
|
||||
width: 0;
|
||||
height: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Animated dashed edge for flow edges */
|
||||
.edge-dashed-animated {
|
||||
stroke-dasharray: 6;
|
||||
stroke-dashoffset: 0;
|
||||
animation: dash 0.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
to {
|
||||
stroke-dashoffset: -12;
|
||||
}
|
||||
}
|
||||
|
||||
/* Edges styling */
|
||||
.react-flow__edge-path {
|
||||
stroke: #10b981; /* Emerald color for edges */
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.react-flow__edge.selected .react-flow__edge-path {
|
||||
stroke: #3b82f6; /* Blue color for selected edges */
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
/* Node handle styling */
|
||||
.react-flow__handle {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #10b981;
|
||||
border: 2px solid #0d9488;
|
||||
}
|
||||
|
||||
.react-flow__handle:hover {
|
||||
background-color: #3b82f6;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
/* Nó em execução */
|
||||
.active-execution-node {
|
||||
animation: pulse-execution 1.5s infinite;
|
||||
filter: drop-shadow(0 0 15px rgba(5, 212, 114, 0.9));
|
||||
z-index: 10;
|
||||
transform: scale(1.03);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes pulse-execution {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(5, 212, 114, 0.9);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 15px rgba(5, 212, 114, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(5, 212, 114, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.react-flow__node[data-is-executing="true"] {
|
||||
filter: drop-shadow(0 0 15px rgba(5, 212, 114, 0.9));
|
||||
transform: scale(1.03);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.react-flow__node[data-is-executing="true"]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
left: -5px;
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
border: 2px solid #05d472;
|
||||
border-radius: 8px;
|
||||
animation: pulse-border 1.5s infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes pulse-border {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
70% {
|
||||
opacity: 0.3;
|
||||
transform: scale(1.03);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
119
frontend/app/agents/workflows/edges/DefaultEdge.tsx
Normal file
119
frontend/app/agents/workflows/edges/DefaultEdge.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/edges/DefaultEdge.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
import {
|
||||
BaseEdge,
|
||||
EdgeLabelRenderer,
|
||||
EdgeProps,
|
||||
getSmoothStepPath,
|
||||
useReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import { Trash2 } from "lucide-react";
|
||||
|
||||
export default function DefaultEdge({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
style = {},
|
||||
selected,
|
||||
}: EdgeProps) {
|
||||
const { setEdges } = useReactFlow();
|
||||
|
||||
const [edgePath, labelX, labelY] = getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
borderRadius: 15,
|
||||
});
|
||||
|
||||
const onEdgeClick = () => {
|
||||
console.log("onEdgeClick", id);
|
||||
setEdges((edges) => edges.filter((edge) => edge.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<svg>
|
||||
<defs>
|
||||
<marker
|
||||
id="arrowhead"
|
||||
viewBox="0 0 10 16"
|
||||
refX="12"
|
||||
refY="8"
|
||||
markerWidth="4"
|
||||
markerHeight="5"
|
||||
orient="auto"
|
||||
>
|
||||
<path d="M 0 0 L 10 8 L 0 16 z" fill="gray" />
|
||||
</marker>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<BaseEdge
|
||||
id={id}
|
||||
path={edgePath}
|
||||
className="edge-dashed-animated"
|
||||
style={{
|
||||
...style,
|
||||
stroke: '#10B981',
|
||||
strokeWidth: 3,
|
||||
}}
|
||||
markerEnd="url(#arrowhead)"
|
||||
/>
|
||||
|
||||
{selected && (
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||
fontSize: 12,
|
||||
pointerEvents: "all",
|
||||
zIndex: 1000,
|
||||
}}
|
||||
className="nodrag nopan"
|
||||
>
|
||||
<button
|
||||
className="rounded-full bg-white p-1 shadow-md"
|
||||
onClick={onEdgeClick}
|
||||
>
|
||||
<Trash2 className="text-red-500" size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
41
frontend/app/agents/workflows/edges/index.ts
Normal file
41
frontend/app/agents/workflows/edges/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/edges/index.ts │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
import type { Edge, EdgeTypes } from "@xyflow/react";
|
||||
|
||||
import DefaultEdge from "./DefaultEdge";
|
||||
|
||||
export const initialEdges = [
|
||||
// { id: "a->c", source: "a", target: "c", animated: true },
|
||||
// { id: "b->d", source: "b", target: "d" },
|
||||
// { id: "c->d", source: "c", target: "d", animated: true },
|
||||
] satisfies Edge[];
|
||||
|
||||
export const edgeTypes = {
|
||||
default: DefaultEdge,
|
||||
} satisfies EdgeTypes;
|
||||
166
frontend/app/agents/workflows/nodes/BaseNode.tsx
Normal file
166
frontend/app/agents/workflows/nodes/BaseNode.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/BaseNode.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
import { Handle, Position } from "@xyflow/react";
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useDnD } from "@/contexts/DnDContext";
|
||||
|
||||
export function BaseNode({
|
||||
selected,
|
||||
hasTarget,
|
||||
children,
|
||||
borderColor,
|
||||
isExecuting
|
||||
}: {
|
||||
selected: boolean;
|
||||
hasTarget: boolean;
|
||||
children: React.ReactNode;
|
||||
borderColor: string;
|
||||
isExecuting?: boolean;
|
||||
}) {
|
||||
const { pointerEvents } = useDnD();
|
||||
|
||||
// Border and background color mapping
|
||||
const colorStyles = {
|
||||
blue: {
|
||||
border: "border-blue-700/70 hover:border-blue-500",
|
||||
gradient: "bg-gradient-to-br from-blue-950/40 to-neutral-900/90",
|
||||
glow: "shadow-[0_0_15px_rgba(59,130,246,0.15)]",
|
||||
selectedGlow: "shadow-[0_0_25px_rgba(59,130,246,0.3)]"
|
||||
},
|
||||
orange: {
|
||||
border: "border-orange-700/70 hover:border-orange-500",
|
||||
gradient: "bg-gradient-to-br from-orange-950/40 to-neutral-900/90",
|
||||
glow: "shadow-[0_0_15px_rgba(249,115,22,0.15)]",
|
||||
selectedGlow: "shadow-[0_0_25px_rgba(249,115,22,0.3)]"
|
||||
},
|
||||
green: {
|
||||
border: "border-green-700/70 hover:border-green-500",
|
||||
gradient: "bg-gradient-to-br from-green-950/40 to-neutral-900/90",
|
||||
glow: "shadow-[0_0_15px_rgba(34,197,94,0.15)]",
|
||||
selectedGlow: "shadow-[0_0_25px_rgba(34,197,94,0.3)]"
|
||||
},
|
||||
red: {
|
||||
border: "border-red-700/70 hover:border-red-500",
|
||||
gradient: "bg-gradient-to-br from-red-950/40 to-neutral-900/90",
|
||||
glow: "shadow-[0_0_15px_rgba(239,68,68,0.15)]",
|
||||
selectedGlow: "shadow-[0_0_25px_rgba(239,68,68,0.3)]"
|
||||
},
|
||||
yellow: {
|
||||
border: "border-yellow-700/70 hover:border-yellow-500",
|
||||
gradient: "bg-gradient-to-br from-yellow-950/40 to-neutral-900/90",
|
||||
glow: "shadow-[0_0_15px_rgba(234,179,8,0.15)]",
|
||||
selectedGlow: "shadow-[0_0_25px_rgba(234,179,8,0.3)]"
|
||||
},
|
||||
purple: {
|
||||
border: "border-purple-700/70 hover:border-purple-500",
|
||||
gradient: "bg-gradient-to-br from-purple-950/40 to-neutral-900/90",
|
||||
glow: "shadow-[0_0_15px_rgba(168,85,247,0.15)]",
|
||||
selectedGlow: "shadow-[0_0_25px_rgba(168,85,247,0.3)]"
|
||||
},
|
||||
indigo: {
|
||||
border: "border-indigo-700/70 hover:border-indigo-500",
|
||||
gradient: "bg-gradient-to-br from-indigo-950/40 to-neutral-900/90",
|
||||
glow: "shadow-[0_0_15px_rgba(99,102,241,0.15)]",
|
||||
selectedGlow: "shadow-[0_0_25px_rgba(99,102,241,0.3)]"
|
||||
},
|
||||
pink: {
|
||||
border: "border-pink-700/70 hover:border-pink-500",
|
||||
gradient: "bg-gradient-to-br from-pink-950/40 to-neutral-900/90",
|
||||
glow: "shadow-[0_0_15px_rgba(236,72,153,0.15)]",
|
||||
selectedGlow: "shadow-[0_0_25px_rgba(236,72,153,0.3)]"
|
||||
},
|
||||
emerald: {
|
||||
border: "border-emerald-700/70 hover:border-emerald-500",
|
||||
gradient: "bg-gradient-to-br from-emerald-950/40 to-neutral-900/90",
|
||||
glow: "shadow-[0_0_15px_rgba(16,185,129,0.15)]",
|
||||
selectedGlow: "shadow-[0_0_25px_rgba(16,185,129,0.3)]"
|
||||
},
|
||||
slate: {
|
||||
border: "border-slate-700/70 hover:border-slate-500",
|
||||
gradient: "bg-gradient-to-br from-slate-800/40 to-neutral-900/90",
|
||||
glow: "shadow-[0_0_15px_rgba(100,116,139,0.15)]",
|
||||
selectedGlow: "shadow-[0_0_25px_rgba(100,116,139,0.3)]"
|
||||
},
|
||||
};
|
||||
|
||||
// Default to blue if color not in mapping
|
||||
const colorStyle = colorStyles[borderColor as keyof typeof colorStyles] || colorStyles.blue;
|
||||
|
||||
// Selected styles
|
||||
const selectedStyle = {
|
||||
border: "border-green-500/90",
|
||||
glow: colorStyle.selectedGlow
|
||||
};
|
||||
|
||||
// Executing styles
|
||||
const executingStyle = {
|
||||
border: "border-emerald-500",
|
||||
glow: "shadow-[0_0_25px_rgba(5,212,114,0.5)]"
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"relative z-0 w-[350px] rounded-2xl p-4 border-2 backdrop-blur-sm transition-all duration-300",
|
||||
"shadow-lg hover:shadow-xl",
|
||||
isExecuting ? executingStyle.glow : selected ? selectedStyle.glow : colorStyle.glow,
|
||||
isExecuting ? executingStyle.border : selected ? selectedStyle.border : colorStyle.border,
|
||||
colorStyle.gradient,
|
||||
isExecuting && "active-execution-node"
|
||||
)}
|
||||
style={{
|
||||
backdropFilter: "blur(12px)",
|
||||
}}
|
||||
data-is-executing={isExecuting ? "true" : "false"}
|
||||
>
|
||||
{hasTarget && (
|
||||
<Handle
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "50%",
|
||||
width: "100%",
|
||||
borderRadius: "15px",
|
||||
height: "100%",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
pointerEvents: pointerEvents === "none" ? "none" : "auto",
|
||||
}}
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
/>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/agent/AgentChatMessageList.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 14, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { ChevronDown, ChevronRight, Copy, Check } from "lucide-react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { useState } from "react";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { InlineDataAttachments } from "@/app/chat/components/InlineDataAttachments";
|
||||
|
||||
interface FunctionMessageContent {
|
||||
title: string;
|
||||
content: string;
|
||||
author?: string;
|
||||
}
|
||||
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
content: {
|
||||
parts: any[];
|
||||
role: string;
|
||||
};
|
||||
author: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface AgentChatMessageListProps {
|
||||
messages: ChatMessage[];
|
||||
agent: Agent;
|
||||
expandedFunctions: Record<string, boolean>;
|
||||
toggleFunctionExpansion: (messageId: string) => void;
|
||||
getMessageText: (message: ChatMessage) => string | FunctionMessageContent;
|
||||
containsMarkdown: (text: string) => boolean;
|
||||
}
|
||||
|
||||
export function AgentChatMessageList({
|
||||
messages,
|
||||
agent,
|
||||
expandedFunctions,
|
||||
toggleFunctionExpansion,
|
||||
getMessageText,
|
||||
containsMarkdown,
|
||||
}: AgentChatMessageListProps) {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Scroll to bottom whenever messages change
|
||||
useEffect(() => {
|
||||
if (messagesEndRef.current) {
|
||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{messages.map((message) => {
|
||||
const messageContent = getMessageText(message);
|
||||
const isExpanded = expandedFunctions[message.id] || false;
|
||||
const isUser = message.author === "user";
|
||||
const agentColor = "bg-emerald-400";
|
||||
const hasFunctionCall = message.content.parts.some(
|
||||
(part) => part.functionCall || part.function_call
|
||||
);
|
||||
const hasFunctionResponse = message.content.parts.some(
|
||||
(part) => part.functionResponse || part.function_response
|
||||
);
|
||||
const isFunctionMessage = hasFunctionCall || hasFunctionResponse;
|
||||
const isTaskExecutor = typeof messageContent === "object" &&
|
||||
"author" in messageContent &&
|
||||
typeof messageContent.author === "string" &&
|
||||
messageContent.author.endsWith("- Task executor");
|
||||
|
||||
const inlineDataParts = message.content.parts.filter(part => part.inline_data);
|
||||
const hasInlineData = inlineDataParts.length > 0;
|
||||
|
||||
const isWorkflowNode = message.author && message.author.startsWith('workflow-node:');
|
||||
const nodeId = isWorkflowNode ? message.author.split(':')[1] : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
className="flex w-full"
|
||||
style={{
|
||||
justifyContent: isUser ? "flex-end" : "flex-start"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex gap-3 max-w-[90%]"
|
||||
style={{
|
||||
flexDirection: isUser ? "row-reverse" : "row"
|
||||
}}
|
||||
>
|
||||
<Avatar className={isUser ? "bg-[#333]" : agentColor}>
|
||||
<AvatarFallback>
|
||||
{isUser ? "U" : agent.name[0] || "A"}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div
|
||||
className={`rounded-lg p-3 ${isFunctionMessage || isTaskExecutor
|
||||
? "bg-[#333] text-emerald-400 font-mono text-sm"
|
||||
: isUser
|
||||
? "bg-emerald-400 text-black"
|
||||
: "bg-[#222] text-white"
|
||||
} overflow-hidden relative group`}
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
maxWidth: "calc(100% - 3rem)",
|
||||
width: "100%",
|
||||
...(isWorkflowNode ? {
|
||||
borderLeft: '3px solid #05d472',
|
||||
boxShadow: '0 0 10px rgba(5, 212, 114, 0.2)'
|
||||
} : {})
|
||||
}}
|
||||
>
|
||||
{isWorkflowNode && (
|
||||
<div className="text-xs text-emerald-500 mb-1 flex items-center space-x-1 bg-emerald-950/30 p-1 rounded">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-3 w-3 mr-1">
|
||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
||||
</svg>
|
||||
<span>Node {nodeId} is running</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isFunctionMessage || isTaskExecutor ? (
|
||||
<div className="w-full">
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer hover:bg-[#444] rounded px-1 py-0.5 transition-colors"
|
||||
onClick={() => toggleFunctionExpansion(message.id)}
|
||||
>
|
||||
{typeof messageContent === "object" &&
|
||||
"title" in messageContent && (
|
||||
<>
|
||||
<div className="flex-1 font-semibold">
|
||||
{(messageContent as FunctionMessageContent).title}
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-5 h-5 text-emerald-400">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isTaskExecutor && (
|
||||
<>
|
||||
<div className="flex-1 font-semibold">
|
||||
Task Execution
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-5 h-5 text-emerald-400">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="mt-2 pt-2 border-t border-[#555]">
|
||||
{typeof messageContent === "object" &&
|
||||
"content" in messageContent && (
|
||||
<div className="max-w-full overflow-x-auto">
|
||||
<pre className="whitespace-pre-wrap text-xs max-w-full" style={{
|
||||
wordWrap: "break-word",
|
||||
maxWidth: "100%",
|
||||
wordBreak: "break-all"
|
||||
}}>
|
||||
{(messageContent as FunctionMessageContent).content}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="markdown-content break-words max-w-full overflow-x-auto">
|
||||
{typeof messageContent === "object" &&
|
||||
"author" in messageContent &&
|
||||
messageContent.author !== "user" &&
|
||||
!isTaskExecutor && (
|
||||
<div className="text-xs text-neutral-400 mb-1">
|
||||
{messageContent.author}
|
||||
</div>
|
||||
)}
|
||||
{((typeof messageContent === "string" &&
|
||||
containsMarkdown(messageContent)) ||
|
||||
(typeof messageContent === "object" &&
|
||||
"content" in messageContent &&
|
||||
typeof messageContent.content === "string" &&
|
||||
containsMarkdown(messageContent.content))) &&
|
||||
!isTaskExecutor ? (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
h1: ({ ...props }) => (
|
||||
<h1 className="text-xl font-bold my-4" {...props} />
|
||||
),
|
||||
h2: ({ ...props }) => (
|
||||
<h2 className="text-lg font-bold my-3" {...props} />
|
||||
),
|
||||
h3: ({ ...props }) => (
|
||||
<h3 className="text-base font-bold my-2" {...props} />
|
||||
),
|
||||
h4: ({ ...props }) => (
|
||||
<h4 className="font-semibold my-2" {...props} />
|
||||
),
|
||||
p: ({ ...props }) => <p className="mb-3" {...props} />,
|
||||
ul: ({ ...props }) => (
|
||||
<ul
|
||||
className="list-disc pl-6 mb-3 space-y-1"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ol: ({ ...props }) => (
|
||||
<ol
|
||||
className="list-decimal pl-6 mb-3 space-y-1"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
li: ({ ...props }) => <li className="mb-1" {...props} />,
|
||||
a: ({ ...props }) => (
|
||||
<a
|
||||
className="text-emerald-400 underline hover:opacity-80 transition-opacity"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
blockquote: ({ ...props }) => (
|
||||
<blockquote
|
||||
className="border-l-4 border-[#444] pl-4 py-1 italic my-3 text-neutral-300"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
code: ({ className, children, ...props }: any) => {
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
const isInline = !match && typeof children === "string" && !children.includes("\n");
|
||||
|
||||
if (isInline) {
|
||||
return (
|
||||
<code
|
||||
className="bg-[#333] px-1.5 py-0.5 rounded text-emerald-400 text-sm font-mono"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-3 relative">
|
||||
<div className="bg-[#1a1a1a] rounded-t-md border-b border-[#333] p-2 text-xs text-neutral-400">
|
||||
<span>{match?.[1] || "Code"}</span>
|
||||
</div>
|
||||
<pre className="bg-[#1a1a1a] p-3 rounded-b-md overflow-x-auto whitespace-pre text-sm">
|
||||
<code {...props}>{children}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
table: ({ ...props }) => (
|
||||
<div className="overflow-x-auto my-3">
|
||||
<table
|
||||
className="min-w-full border border-[#333] rounded"
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
thead: ({ ...props }) => (
|
||||
<thead className="bg-[#1a1a1a]" {...props} />
|
||||
),
|
||||
tbody: ({ ...props }) => <tbody {...props} />,
|
||||
tr: ({ ...props }) => (
|
||||
<tr
|
||||
className="border-b border-[#333] last:border-0"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
th: ({ ...props }) => (
|
||||
<th
|
||||
className="px-4 py-2 text-left text-xs font-semibold text-neutral-300"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
td: ({ ...props }) => (
|
||||
<td className="px-4 py-2 text-sm" {...props} />
|
||||
),
|
||||
}}
|
||||
>
|
||||
{typeof messageContent === "string"
|
||||
? messageContent
|
||||
: messageContent.content}
|
||||
</ReactMarkdown>
|
||||
) : (
|
||||
<div className="whitespace-pre-wrap">
|
||||
{typeof messageContent === "string"
|
||||
? messageContent
|
||||
: messageContent.content}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasInlineData && (
|
||||
<InlineDataAttachments parts={inlineDataParts} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* Empty div at the end for auto-scrolling */}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,631 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/agent/AgentForm.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* eslint-disable jsx-a11y/alt-text */
|
||||
import { useEdges, useNodes } from "@xyflow/react";
|
||||
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { listAgents, listFolders, Folder, getAgent } from "@/services/agentService";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { User, Loader2, Search, FolderIcon, Trash2, Play, MessageSquare, PlayIcon, Plus, X } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { AgentForm as GlobalAgentForm } from "@/app/agents/forms/AgentForm";
|
||||
import { ApiKey, listApiKeys } from "@/services/agentService";
|
||||
import { listMCPServers } from "@/services/mcpServerService";
|
||||
import { availableModels } from "@/types/aiModels";
|
||||
import { MCPServer } from "@/types/mcpServer";
|
||||
import { AgentTestChatModal } from "./AgentTestChatModal";
|
||||
import { sanitizeAgentName, escapePromptBraces } from "@/lib/utils";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const user = typeof window !== "undefined" ? JSON.parse(localStorage.getItem("user") || '{}') : {};
|
||||
const clientId: string = user?.client_id ? String(user.client_id) : "";
|
||||
|
||||
const agentListStyles = {
|
||||
scrollbarWidth: 'none', /* Firefox */
|
||||
msOverflowStyle: 'none', /* IE and Edge */
|
||||
'::-webkit-scrollbar': {
|
||||
display: 'none' /* Chrome, Safari and Opera */
|
||||
}
|
||||
};
|
||||
|
||||
export function AgentForm({ selectedNode, handleUpdateNode, setEdges, setIsOpen, setSelectedNode }: {
|
||||
selectedNode: any;
|
||||
handleUpdateNode: any;
|
||||
setEdges: any;
|
||||
setIsOpen: any;
|
||||
setSelectedNode: any;
|
||||
}) {
|
||||
const [node, setNode] = useState(selectedNode);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadingFolders, setLoadingFolders] = useState(true);
|
||||
const [loadingCurrentAgent, setLoadingCurrentAgent] = useState(false);
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [allAgents, setAllAgents] = useState<Agent[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [folders, setFolders] = useState<Folder[]>([]);
|
||||
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null);
|
||||
const [selectedAgentType, setSelectedAgentType] = useState<string | null>(null);
|
||||
const [agentTypes, setAgentTypes] = useState<string[]>([]);
|
||||
const [agentFolderId, setAgentFolderId] = useState<string | null>(null);
|
||||
const edges = useEdges();
|
||||
const nodes = useNodes();
|
||||
const [isAgentDialogOpen, setIsAgentDialogOpen] = useState(false);
|
||||
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
|
||||
const [availableMCPs, setAvailableMCPs] = useState<MCPServer[]>([]);
|
||||
const [newAgent, setNewAgent] = useState<Partial<Agent>>({
|
||||
client_id: clientId || "",
|
||||
name: "",
|
||||
description: "",
|
||||
type: "llm",
|
||||
model: "openai/gpt-4.1-nano",
|
||||
instruction: "",
|
||||
api_key_id: "",
|
||||
config: {
|
||||
tools: [],
|
||||
mcp_servers: [],
|
||||
custom_mcp_servers: [],
|
||||
custom_tools: { http_tools: [] },
|
||||
sub_agents: [],
|
||||
agent_tools: [],
|
||||
},
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isTestModalOpen, setIsTestModalOpen] = useState(false);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
// Access the canvas reference from localStorage
|
||||
const canvasRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// When the component is mounted, check if there is a canvas reference in the global context
|
||||
if (typeof window !== "undefined") {
|
||||
const workflowsPage = document.querySelector('[data-workflow-page="true"]');
|
||||
if (workflowsPage) {
|
||||
// If we are on the workflows page, try to access the canvas ref
|
||||
const canvasElement = workflowsPage.querySelector('[data-canvas-ref="true"]');
|
||||
if (canvasElement && (canvasElement as any).__reactRef) {
|
||||
canvasRef.current = (canvasElement as any).__reactRef.current;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const connectedNode = useMemo(() => {
|
||||
const edge = edges.find((edge: any) => edge.source === selectedNode.id);
|
||||
if (!edge) return null;
|
||||
const node = nodes.find((node: any) => node.id === edge.target);
|
||||
return node || null;
|
||||
}, [edges, nodes, selectedNode.id]);
|
||||
|
||||
const currentAgent = typeof window !== "undefined" ?
|
||||
JSON.parse(localStorage.getItem("current_workflow_agent") || '{}') : {};
|
||||
const currentAgentId = currentAgent?.id;
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedNode) {
|
||||
setNode(selectedNode);
|
||||
}
|
||||
}, [selectedNode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clientId) return;
|
||||
setLoadingFolders(true);
|
||||
listFolders(clientId)
|
||||
.then((res) => {
|
||||
setFolders(res.data);
|
||||
})
|
||||
.catch((error) => console.error("Error loading folders:", error))
|
||||
.finally(() => setLoadingFolders(false));
|
||||
}, [clientId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentAgentId || !clientId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingCurrentAgent(true);
|
||||
|
||||
getAgent(currentAgentId, clientId)
|
||||
.then((res) => {
|
||||
const agent = res.data;
|
||||
if (agent.folder_id) {
|
||||
setAgentFolderId(agent.folder_id);
|
||||
setSelectedFolderId(agent.folder_id);
|
||||
}
|
||||
})
|
||||
.catch((error) => console.error("Error loading current agent:", error))
|
||||
.finally(() => setLoadingCurrentAgent(false));
|
||||
}, [currentAgentId, clientId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clientId) return;
|
||||
|
||||
if (loadingFolders || loadingCurrentAgent) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
listAgents(clientId, 0, 100, selectedFolderId || undefined)
|
||||
.then((res) => {
|
||||
const filteredAgents = res.data.filter((agent: Agent) => agent.id !== currentAgentId);
|
||||
setAllAgents(filteredAgents);
|
||||
setAgents(filteredAgents);
|
||||
|
||||
// Extract unique agent types
|
||||
const types = [...new Set(filteredAgents.map(agent => agent.type))].filter(Boolean);
|
||||
setAgentTypes(types);
|
||||
})
|
||||
.catch((error) => console.error("Error loading agents:", error))
|
||||
.finally(() => setLoading(false));
|
||||
}, [clientId, currentAgentId, selectedFolderId, loadingFolders, loadingCurrentAgent]);
|
||||
|
||||
useEffect(() => {
|
||||
// Apply all filters: search, folder, and type
|
||||
let filtered = allAgents;
|
||||
|
||||
// Search filter is applied in a separate effect
|
||||
if (searchQuery.trim() !== "") {
|
||||
filtered = filtered.filter((agent) =>
|
||||
agent.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
agent.description?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
// Apply agent type filter
|
||||
if (selectedAgentType) {
|
||||
filtered = filtered.filter(agent => agent.type === selectedAgentType);
|
||||
}
|
||||
|
||||
setAgents(filtered);
|
||||
}, [searchQuery, selectedAgentType, allAgents]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clientId) return;
|
||||
listApiKeys(clientId).then((res) => setApiKeys(res.data));
|
||||
listMCPServers().then((res) => setAvailableMCPs(res.data));
|
||||
}, [clientId]);
|
||||
|
||||
const handleDeleteEdge = useCallback(() => {
|
||||
const id = edges.find((edge: any) => edge.source === selectedNode.id)?.id;
|
||||
setEdges((edges: any) => {
|
||||
const left = edges.filter((edge: any) => edge.id !== id);
|
||||
return left;
|
||||
});
|
||||
}, [nodes, edges, selectedNode, setEdges]);
|
||||
|
||||
const handleSelectAgent = (agent: Agent) => {
|
||||
const updatedNode = {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
agent,
|
||||
},
|
||||
};
|
||||
setNode(updatedNode);
|
||||
handleUpdateNode(updatedNode);
|
||||
};
|
||||
|
||||
const getAgentTypeName = (type: string) => {
|
||||
const agentTypes: Record<string, string> = {
|
||||
llm: "LLM Agent",
|
||||
a2a: "A2A Agent",
|
||||
sequential: "Sequential Agent",
|
||||
parallel: "Parallel Agent",
|
||||
loop: "Loop Agent",
|
||||
workflow: "Workflow Agent",
|
||||
task: "Task Agent",
|
||||
};
|
||||
return agentTypes[type] || type;
|
||||
};
|
||||
|
||||
const handleOpenAgentDialog = () => {
|
||||
setNewAgent({
|
||||
client_id: clientId || "",
|
||||
name: "",
|
||||
description: "",
|
||||
type: "llm",
|
||||
model: "openai/gpt-4.1-nano",
|
||||
instruction: "",
|
||||
api_key_id: "",
|
||||
config: {
|
||||
tools: [],
|
||||
mcp_servers: [],
|
||||
custom_mcp_servers: [],
|
||||
custom_tools: { http_tools: [] },
|
||||
sub_agents: [],
|
||||
agent_tools: [],
|
||||
},
|
||||
folder_id: selectedFolderId || undefined,
|
||||
});
|
||||
setIsAgentDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveAgent = async (agentData: Partial<Agent>) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const sanitizedData = {
|
||||
...agentData,
|
||||
client_id: clientId,
|
||||
name: agentData.name ? sanitizeAgentName(agentData.name) : agentData.name,
|
||||
instruction: agentData.instruction ? escapePromptBraces(agentData.instruction) : agentData.instruction
|
||||
};
|
||||
|
||||
if (isEditMode && node.data.agent?.id) {
|
||||
// Update existing agent
|
||||
const { updateAgent } = await import("@/services/agentService");
|
||||
const updated = await updateAgent(node.data.agent.id, sanitizedData as any);
|
||||
|
||||
// Refresh the agent list
|
||||
const res = await listAgents(clientId, 0, 100, selectedFolderId || undefined);
|
||||
const filteredAgents = res.data.filter((agent: Agent) => agent.id !== currentAgentId);
|
||||
setAllAgents(filteredAgents);
|
||||
setAgents(filteredAgents);
|
||||
|
||||
if (updated.data) {
|
||||
handleSelectAgent(updated.data);
|
||||
}
|
||||
} else {
|
||||
// Create new agent
|
||||
const { createAgent } = await import("@/services/agentService");
|
||||
const created = await createAgent(sanitizedData as any);
|
||||
|
||||
const res = await listAgents(clientId, 0, 100, selectedFolderId || undefined);
|
||||
const filteredAgents = res.data.filter((agent: Agent) => agent.id !== currentAgentId);
|
||||
setAllAgents(filteredAgents);
|
||||
setAgents(filteredAgents);
|
||||
|
||||
if (created.data) {
|
||||
handleSelectAgent(created.data);
|
||||
}
|
||||
}
|
||||
|
||||
setIsAgentDialogOpen(false);
|
||||
setIsEditMode(false);
|
||||
} catch (e) {
|
||||
console.error("Error saving agent:", e);
|
||||
setIsAgentDialogOpen(false);
|
||||
setIsEditMode(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFolderChange = (value: string) => {
|
||||
setSelectedFolderId(value === "all" ? null : value);
|
||||
};
|
||||
|
||||
const handleAgentTypeChange = (value: string) => {
|
||||
setSelectedAgentType(value === "all" ? null : value);
|
||||
};
|
||||
|
||||
const getFolderNameById = (id: string) => {
|
||||
const folder = folders.find((f) => f.id === id);
|
||||
return folder?.name || id;
|
||||
};
|
||||
|
||||
const handleEditAgent = () => {
|
||||
if (!node.data.agent) return;
|
||||
|
||||
setNewAgent({
|
||||
...node.data.agent,
|
||||
client_id: clientId || "",
|
||||
});
|
||||
|
||||
setIsEditMode(true);
|
||||
setIsAgentDialogOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isAgentDialogOpen && (
|
||||
<GlobalAgentForm
|
||||
open={isAgentDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setIsAgentDialogOpen(open);
|
||||
if (!open) setIsEditMode(false);
|
||||
}}
|
||||
initialValues={newAgent}
|
||||
apiKeys={apiKeys}
|
||||
availableModels={availableModels}
|
||||
availableMCPs={availableMCPs}
|
||||
agents={allAgents}
|
||||
onOpenApiKeysDialog={() => {}}
|
||||
onOpenMCPDialog={() => {}}
|
||||
onOpenCustomMCPDialog={() => {}}
|
||||
onSave={handleSaveAgent}
|
||||
isLoading={isLoading}
|
||||
getAgentNameById={(id) => allAgents.find((a) => a.id === id)?.name || id}
|
||||
clientId={clientId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Agent Test Chat Modal - moved outside of nested divs to render properly */}
|
||||
{isTestModalOpen && node.data.agent && (
|
||||
<AgentTestChatModal
|
||||
open={isTestModalOpen}
|
||||
onOpenChange={setIsTestModalOpen}
|
||||
agent={node.data.agent}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="p-4 border-b border-neutral-700 flex-shrink-0">
|
||||
<div className="mb-3">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-neutral-500" />
|
||||
<Input
|
||||
placeholder="Search agents..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 bg-neutral-800 border-neutral-700 text-neutral-200 focus-visible:ring-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1">
|
||||
<Select
|
||||
value={selectedFolderId ? selectedFolderId : "all"}
|
||||
onValueChange={handleFolderChange}
|
||||
>
|
||||
<SelectTrigger className="w-full h-9 bg-neutral-800 border-neutral-700 text-neutral-200 focus:ring-emerald-500 focus:ring-offset-0">
|
||||
<SelectValue placeholder="All folders" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||
<SelectItem value="all">All folders</SelectItem>
|
||||
{folders.map((folder) => (
|
||||
<SelectItem key={folder.id} value={folder.id}>
|
||||
{folder.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<Select
|
||||
value={selectedAgentType ? selectedAgentType : "all"}
|
||||
onValueChange={handleAgentTypeChange}
|
||||
>
|
||||
<SelectTrigger className="w-full h-9 bg-neutral-800 border-neutral-700 text-neutral-200 focus:ring-emerald-500 focus:ring-offset-0">
|
||||
<SelectValue placeholder="All types" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||
<SelectItem value="all">All types</SelectItem>
|
||||
{agentTypes.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{getAgentTypeName(type)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col flex-1 min-h-0">
|
||||
<div className="px-4 pt-4 pb-2 flex items-center justify-between flex-shrink-0">
|
||||
<h3 className="text-md font-medium text-neutral-200">
|
||||
{searchQuery ? "Search Results" : "Select an Agent"}
|
||||
</h3>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 bg-emerald-800 hover:bg-emerald-700 border-emerald-700 text-emerald-100"
|
||||
onClick={() => {
|
||||
setNewAgent({
|
||||
id: "",
|
||||
name: "",
|
||||
client_id: clientId || "",
|
||||
type: "llm",
|
||||
model: "",
|
||||
config: {},
|
||||
description: "",
|
||||
});
|
||||
setIsEditMode(false);
|
||||
setIsAgentDialogOpen(true);
|
||||
}}
|
||||
aria-label="New Agent"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-4 scrollbar-hide">
|
||||
<div className="space-y-2 pr-2">
|
||||
{agents.length > 0 ? (
|
||||
agents.map((agent) => (
|
||||
<div
|
||||
key={agent.id}
|
||||
className={`p-3 rounded-md cursor-pointer transition-colors group relative ${
|
||||
node.data.agent?.id === agent.id
|
||||
? "bg-emerald-800/20 border border-emerald-600/40"
|
||||
: "bg-neutral-800 hover:bg-neutral-700 border border-transparent"
|
||||
}`}
|
||||
onClick={() => handleSelectAgent(agent)}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="bg-neutral-700 rounded-full p-1.5 flex-shrink-0">
|
||||
<User size={18} className="text-neutral-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium text-neutral-200 truncate">{agent.name}</h3>
|
||||
<div
|
||||
className="ml-auto text-neutral-400 opacity-0 group-hover:opacity-100 hover:text-yellow-500 transition-colors p-1 rounded hover:bg-yellow-900/20"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setNewAgent({
|
||||
...agent,
|
||||
client_id: clientId || "",
|
||||
});
|
||||
setIsEditMode(true);
|
||||
setIsAgentDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
|
||||
<path d="m15 5 4 4" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs bg-neutral-700 text-emerald-400 border-neutral-600"
|
||||
>
|
||||
{getAgentTypeName(agent.type)}
|
||||
</Badge>
|
||||
{agent.model && (
|
||||
<span className="text-xs text-neutral-400">{agent.model}</span>
|
||||
)}
|
||||
</div>
|
||||
{agent.description && (
|
||||
<p className="text-sm text-neutral-400 mt-1.5 line-clamp-2">
|
||||
{agent.description.slice(0, 30)} {agent.description.length > 30 ? "..." : ""}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-4 text-neutral-400">
|
||||
No agents found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{node.data.agent && (
|
||||
<div className="p-4 border-t border-neutral-700 flex-shrink-0">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-md font-medium text-neutral-200">Selected Agent</h3>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 bg-neutral-700 hover:bg-neutral-600 border-neutral-600 text-neutral-200"
|
||||
onClick={() => {
|
||||
handleUpdateNode({
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
agent: null,
|
||||
},
|
||||
});
|
||||
}}
|
||||
aria-label="Clear agent"
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 bg-neutral-700 hover:bg-neutral-600 border-neutral-600 text-neutral-200"
|
||||
onClick={handleEditAgent}
|
||||
aria-label="Edit agent"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
|
||||
<path d="m15 5 4 4" />
|
||||
</svg>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 bg-emerald-800 hover:bg-emerald-700 border-emerald-700 text-emerald-100"
|
||||
onClick={() => setIsTestModalOpen(true)}
|
||||
aria-label="Test agent"
|
||||
>
|
||||
<PlayIcon size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-md bg-emerald-800/20 border border-emerald-600/40">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="bg-emerald-900/50 rounded-full p-1.5 flex-shrink-0">
|
||||
<User size={18} className="text-emerald-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium text-neutral-200 truncate">{node.data.agent.name}</h3>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs bg-emerald-900/50 text-emerald-400 border-emerald-700/50"
|
||||
>
|
||||
{getAgentTypeName(node.data.agent.type)}
|
||||
</Badge>
|
||||
{node.data.agent.model && (
|
||||
<span className="text-xs text-neutral-400">{node.data.agent.model}</span>
|
||||
)}
|
||||
</div>
|
||||
{node.data.agent.description && (
|
||||
<p className="text-sm text-neutral-400 mt-1.5 line-clamp-2">
|
||||
{node.data.agent.description.slice(0, 30)} {node.data.agent.description.length > 30 ? "..." : ""}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/agent/AgentNode.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Handle, NodeProps, Position, useEdges } from "@xyflow/react";
|
||||
import { MessageCircle, User, Code, ExternalLink, Workflow, GitBranch, RefreshCw, BookOpenCheck, ArrowRight } from "lucide-react";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { BaseNode } from "../../BaseNode";
|
||||
|
||||
export function AgentNode(props: NodeProps) {
|
||||
const { selected, data } = props;
|
||||
|
||||
const edges = useEdges();
|
||||
|
||||
const isHandleConnected = (handleId: string) => {
|
||||
return edges.some(
|
||||
(edge) => edge.source === props.id && edge.sourceHandle === handleId
|
||||
);
|
||||
};
|
||||
|
||||
const isBottomHandleConnected = isHandleConnected("bottom-handle");
|
||||
|
||||
const agent = data.agent as Agent | undefined;
|
||||
const isExecuting = data.isExecuting as boolean | undefined;
|
||||
|
||||
const getAgentTypeName = (type: string) => {
|
||||
const agentTypes: Record<string, string> = {
|
||||
llm: "LLM Agent",
|
||||
a2a: "A2A Agent",
|
||||
sequential: "Sequential Agent",
|
||||
parallel: "Parallel Agent",
|
||||
loop: "Loop Agent",
|
||||
workflow: "Workflow Agent",
|
||||
task: "Task Agent",
|
||||
};
|
||||
return agentTypes[type] || type;
|
||||
};
|
||||
|
||||
const getAgentTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "llm":
|
||||
return <Code className="h-4 w-4 text-green-400" />;
|
||||
case "a2a":
|
||||
return <ExternalLink className="h-4 w-4 text-indigo-400" />;
|
||||
case "sequential":
|
||||
return <Workflow className="h-4 w-4 text-yellow-400" />;
|
||||
case "parallel":
|
||||
return <GitBranch className="h-4 w-4 text-purple-400" />;
|
||||
case "loop":
|
||||
return <RefreshCw className="h-4 w-4 text-orange-400" />;
|
||||
case "workflow":
|
||||
return <Workflow className="h-4 w-4 text-blue-400" />;
|
||||
case "task":
|
||||
return <BookOpenCheck className="h-4 w-4 text-red-400" />;
|
||||
default:
|
||||
return <User className="h-4 w-4 text-neutral-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getModelBadgeColor = (model: string) => {
|
||||
if (model?.includes('gpt-4')) return 'bg-green-900/30 text-green-400 border-green-600/30';
|
||||
if (model?.includes('gpt-3')) return 'bg-yellow-900/30 text-yellow-400 border-yellow-600/30';
|
||||
if (model?.includes('claude')) return 'bg-orange-900/30 text-orange-400 border-orange-600/30';
|
||||
if (model?.includes('gemini')) return 'bg-blue-900/30 text-blue-400 border-blue-600/30';
|
||||
return 'bg-neutral-800 text-neutral-400 border-neutral-600/50';
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseNode hasTarget={true} selected={selected || false} borderColor="blue" isExecuting={isExecuting}>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-900/40 shadow-sm">
|
||||
<User className="h-5 w-5 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-medium text-blue-400">
|
||||
{data.label as string}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{agent ? (
|
||||
<div className="mb-3 rounded-lg border border-blue-700/40 bg-blue-950/10 p-3 transition-all duration-200 hover:border-blue-600/50 hover:bg-blue-900/10">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center">
|
||||
{getAgentTypeIcon(agent.type)}
|
||||
<span className="ml-1.5 font-medium text-white">{agent.name}</span>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="px-1.5 py-0 text-xs bg-blue-900/30 text-blue-400 border-blue-700/40"
|
||||
>
|
||||
{getAgentTypeName(agent.type)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{agent.model && (
|
||||
<div className="mt-2 flex items-center">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn("px-1.5 py-0 text-xs", getModelBadgeColor(agent.model))}
|
||||
>
|
||||
{agent.model}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{agent.description && (
|
||||
<p className="mt-2 text-xs text-neutral-400 line-clamp-2">
|
||||
{agent.description.slice(0, 30)} {agent.description.length > 30 && '...'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-3 flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-blue-700/40 bg-blue-950/10 p-5 text-center transition-all duration-200 hover:border-blue-600/50 hover:bg-blue-900/20">
|
||||
<User className="h-8 w-8 text-blue-700/50 mb-2" />
|
||||
<p className="text-blue-400">Select an agent</p>
|
||||
<p className="mt-1 text-xs text-neutral-500">Click to configure</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 flex items-center justify-end text-sm text-neutral-400 transition-colors">
|
||||
<div className="flex items-center space-x-1 rounded-md py-1 px-2">
|
||||
<span>Next step</span>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<Handle
|
||||
className={cn(
|
||||
"!w-3 !h-3 !rounded-full transition-all duration-300",
|
||||
isBottomHandleConnected ? "!bg-blue-500 !border-blue-400" : "!bg-neutral-400 !border-neutral-500",
|
||||
selected && isBottomHandleConnected && "!bg-blue-400 !border-blue-300"
|
||||
)}
|
||||
style={{
|
||||
right: "-8px",
|
||||
top: "calc(100% - 25px)",
|
||||
}}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="bottom-handle"
|
||||
/>
|
||||
</div>
|
||||
</BaseNode>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/agent/AgentTestChatModal.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useAgentWebSocket } from "@/hooks/use-agent-webSocket";
|
||||
import { getAccessTokenFromCookie, cn } from "@/lib/utils";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { ChatInput } from "@/app/chat/components/ChatInput";
|
||||
import { ChatMessage as ChatMessageComponent } from "@/app/chat/components/ChatMessage";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { ChatPart } from "@/services/sessionService";
|
||||
import { FileData } from "@/lib/file-utils";
|
||||
import { X, User, Bot, Zap, MessageSquare, Loader2, Code, ExternalLink, Workflow, RefreshCw } from "lucide-react";
|
||||
|
||||
interface FunctionMessageContent {
|
||||
title: string;
|
||||
content: string;
|
||||
author?: string;
|
||||
}
|
||||
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
content: any;
|
||||
author: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface AgentTestChatModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
agent: Agent;
|
||||
canvasRef?: React.RefObject<any>;
|
||||
}
|
||||
|
||||
export function AgentTestChatModal({ open, onOpenChange, agent, canvasRef }: AgentTestChatModalProps) {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const [expandedFunctions, setExpandedFunctions] = useState<Record<string, boolean>>({});
|
||||
const [isInitializing, setIsInitializing] = useState(true);
|
||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const user = typeof window !== "undefined" ? JSON.parse(localStorage.getItem("user") || "{}") : {};
|
||||
const clientId = user?.client_id || "test";
|
||||
|
||||
const generateExternalId = () => {
|
||||
const now = new Date();
|
||||
return (
|
||||
now.getFullYear().toString() +
|
||||
(now.getMonth() + 1).toString().padStart(2, "0") +
|
||||
now.getDate().toString().padStart(2, "0") +
|
||||
now.getHours().toString().padStart(2, "0") +
|
||||
now.getMinutes().toString().padStart(2, "0") +
|
||||
now.getSeconds().toString().padStart(2, "0") +
|
||||
now.getMilliseconds().toString().padStart(3, "0")
|
||||
);
|
||||
};
|
||||
|
||||
const [externalId, setExternalId] = useState(generateExternalId());
|
||||
const jwt = getAccessTokenFromCookie();
|
||||
|
||||
const onEvent = useCallback((event: any) => {
|
||||
setMessages((prev) => [...prev, event]);
|
||||
|
||||
// Check if the message comes from a workflow node and highlight the node
|
||||
// only if the canvasRef is available (called from Test Workflow on the main page)
|
||||
if (event.author && event.author.startsWith('workflow-node:') && canvasRef?.current) {
|
||||
const nodeId = event.author.split(':')[1];
|
||||
canvasRef.current.setActiveExecutionNodeId(nodeId);
|
||||
}
|
||||
}, [canvasRef]);
|
||||
|
||||
const onTurnComplete = useCallback(() => {
|
||||
setIsSending(false);
|
||||
}, []);
|
||||
|
||||
const { sendMessage: wsSendMessage, disconnect } = useAgentWebSocket({
|
||||
agentId: agent.id,
|
||||
externalId,
|
||||
jwt,
|
||||
onEvent,
|
||||
onTurnComplete,
|
||||
});
|
||||
|
||||
// Handle ESC key to close the panel
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && open) {
|
||||
onOpenChange(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [onOpenChange, open]);
|
||||
|
||||
// Show initialization state for better UX
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setIsInitializing(true);
|
||||
const timer = setTimeout(() => {
|
||||
setIsInitializing(false);
|
||||
}, 1200);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [open, externalId]);
|
||||
|
||||
const handleRestartChat = () => {
|
||||
if (disconnect) disconnect();
|
||||
setMessages([]);
|
||||
setExpandedFunctions({});
|
||||
setExternalId(generateExternalId());
|
||||
setIsInitializing(true);
|
||||
|
||||
// Short delay to show the initialization status
|
||||
const timer = setTimeout(() => {
|
||||
setIsInitializing(false);
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
const handleSendMessageWithFiles = (message: string, files?: FileData[]) => {
|
||||
if ((!message.trim() && (!files || files.length === 0))) return;
|
||||
setIsSending(true);
|
||||
|
||||
const messageParts: ChatPart[] = [];
|
||||
|
||||
if (message.trim()) {
|
||||
messageParts.push({ text: message });
|
||||
}
|
||||
|
||||
if (files && files.length > 0) {
|
||||
files.forEach(file => {
|
||||
messageParts.push({
|
||||
inline_data: {
|
||||
data: file.data,
|
||||
mime_type: file.content_type,
|
||||
metadata: {
|
||||
filename: file.filename
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: `temp-${Date.now()}`,
|
||||
content: {
|
||||
parts: messageParts,
|
||||
role: "user"
|
||||
},
|
||||
author: "user",
|
||||
timestamp: Date.now() / 1000,
|
||||
},
|
||||
]);
|
||||
|
||||
wsSendMessage(message, files);
|
||||
};
|
||||
|
||||
const containsMarkdown = (text: string): boolean => {
|
||||
if (!text || text.length < 3) return false;
|
||||
const markdownPatterns = [
|
||||
/[*_]{1,2}[^*_]+[*_]{1,2}/, // bold/italic
|
||||
/\[[^\]]+\]\([^)]+\)/, // links
|
||||
/^#{1,6}\s/m, // headers
|
||||
/^[-*+]\s/m, // unordered lists
|
||||
/^[0-9]+\.\s/m, // ordered lists
|
||||
/^>\s/m, // block quotes
|
||||
/`[^`]+`/, // inline code
|
||||
/```[\s\S]*?```/, // code blocks
|
||||
/^\|(.+\|)+$/m, // tables
|
||||
/!\[[^\]]*\]\([^)]+\)/, // images
|
||||
];
|
||||
return markdownPatterns.some((pattern) => pattern.test(text));
|
||||
};
|
||||
|
||||
const getMessageText = (message: ChatMessage): string | FunctionMessageContent => {
|
||||
const author = message.author;
|
||||
const parts = message.content.parts;
|
||||
if (!parts || parts.length === 0) return "Empty content";
|
||||
const functionCallPart = parts.find((part: any) => part.functionCall || part.function_call);
|
||||
const functionResponsePart = parts.find((part: any) => part.functionResponse || part.function_response);
|
||||
|
||||
const inlineDataParts = parts.filter((part: any) => part.inline_data);
|
||||
|
||||
if (functionCallPart) {
|
||||
const funcCall = functionCallPart.functionCall || functionCallPart.function_call || {};
|
||||
const args = funcCall.args || {};
|
||||
const name = funcCall.name || "unknown";
|
||||
const id = funcCall.id || "no-id";
|
||||
return {
|
||||
author,
|
||||
title: `📞 Function call: ${name}`,
|
||||
content: `ID: ${id}\nArgs: ${Object.keys(args).length > 0 ? `\n${JSON.stringify(args, null, 2)}` : "{}"}`,
|
||||
} as FunctionMessageContent;
|
||||
}
|
||||
if (functionResponsePart) {
|
||||
const funcResponse = functionResponsePart.functionResponse || functionResponsePart.function_response || {};
|
||||
const response = funcResponse.response || {};
|
||||
const name = funcResponse.name || "unknown";
|
||||
const id = funcResponse.id || "no-id";
|
||||
const status = response.status || "unknown";
|
||||
const statusEmoji = status === "error" ? "❌" : "✅";
|
||||
let resultText = "";
|
||||
if (status === "error") {
|
||||
resultText = `Error: ${response.error_message || "Unknown error"}`;
|
||||
} else if (response.report) {
|
||||
resultText = `Result: ${response.report}`;
|
||||
} else if (response.result && response.result.content) {
|
||||
const content = response.result.content;
|
||||
if (Array.isArray(content) && content.length > 0 && content[0].text) {
|
||||
try {
|
||||
const textContent = content[0].text;
|
||||
const parsedJson = JSON.parse(textContent);
|
||||
resultText = `Result: \n${JSON.stringify(parsedJson, null, 2)}`;
|
||||
} catch (e) {
|
||||
resultText = `Result: ${content[0].text}`;
|
||||
}
|
||||
} else {
|
||||
resultText = `Result:\n${JSON.stringify(response, null, 2)}`;
|
||||
}
|
||||
} else {
|
||||
resultText = `Result:\n${JSON.stringify(response, null, 2)}`;
|
||||
}
|
||||
return {
|
||||
author,
|
||||
title: `${statusEmoji} Function response: ${name}`,
|
||||
content: `ID: ${id}\n${resultText}`,
|
||||
} as FunctionMessageContent;
|
||||
}
|
||||
|
||||
if (parts.length === 1 && parts[0].text) {
|
||||
return {
|
||||
author,
|
||||
content: parts[0].text,
|
||||
title: "Message",
|
||||
} as FunctionMessageContent;
|
||||
}
|
||||
const textParts = parts.filter((part: any) => part.text).map((part: any) => part.text).filter((text: string) => text);
|
||||
if (textParts.length > 0) {
|
||||
return {
|
||||
author,
|
||||
content: textParts.join("\n\n"),
|
||||
title: "Message",
|
||||
} as FunctionMessageContent;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(parts, null, 2).replace(/\\n/g, "\n");
|
||||
} catch (error) {
|
||||
return "Unable to interpret message content";
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFunctionExpansion = (messageId: string) => {
|
||||
setExpandedFunctions((prev) => ({ ...prev, [messageId]: !prev[messageId] }));
|
||||
};
|
||||
|
||||
const getAgentTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "llm":
|
||||
return <Code className="h-4 w-4 text-green-400" />;
|
||||
case "a2a":
|
||||
return <ExternalLink className="h-4 w-4 text-indigo-400" />;
|
||||
case "sequential":
|
||||
case "workflow":
|
||||
return <Workflow className="h-4 w-4 text-blue-400" />;
|
||||
default:
|
||||
return <Bot className="h-4 w-4 text-emerald-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
// Scroll to bottom whenever messages change
|
||||
useEffect(() => {
|
||||
if (messagesContainerRef.current) {
|
||||
messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
// Use React Portal to render directly to document body, bypassing all parent containers
|
||||
const modalContent = (
|
||||
<>
|
||||
{/* Overlay for mobile */}
|
||||
<div
|
||||
className="md:hidden fixed inset-0 bg-black bg-opacity-50 z-[15] transition-opacity duration-300"
|
||||
onClick={() => onOpenChange(false)}
|
||||
/>
|
||||
|
||||
{/* Side panel */}
|
||||
<div
|
||||
className="fixed right-0 top-0 z-[1000] h-full w-[450px] bg-gradient-to-b from-neutral-900 to-neutral-950 border-l border-neutral-800 shadow-2xl flex flex-col transition-all duration-300 ease-in-out transform"
|
||||
style={{
|
||||
transform: open ? 'translateX(0)' : 'translateX(100%)',
|
||||
boxShadow: '0 0 25px rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex-shrink-0 p-5 bg-gradient-to-r from-neutral-900 to-neutral-800 border-b border-neutral-800">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center mb-1">
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-emerald-600 to-emerald-900 flex items-center justify-center shadow-lg mr-3">
|
||||
{getAgentTypeIcon(agent.type)}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">{agent.name}</h2>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge
|
||||
className="bg-emerald-900/40 text-emerald-400 border border-emerald-700/50 px-2"
|
||||
>
|
||||
{agent.type.toUpperCase()} Agent
|
||||
</Badge>
|
||||
{agent.model && (
|
||||
<span className="text-xs text-neutral-400 bg-neutral-800/60 px-2 py-0.5 rounded-md">
|
||||
{agent.model}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={handleRestartChat}
|
||||
className="p-1.5 rounded-full hover:bg-neutral-700/50 text-neutral-400 hover:text-white transition-colors group relative"
|
||||
title="Restart chat"
|
||||
disabled={isInitializing}
|
||||
>
|
||||
<RefreshCw size={18} className={isInitializing ? "animate-spin text-emerald-400" : ""} />
|
||||
<span className="absolute -bottom-8 right-0 bg-neutral-800 text-neutral-200 text-xs rounded py-1 px-2 opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap">
|
||||
Restart chat
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="p-1.5 rounded-full hover:bg-neutral-700/50 text-neutral-400 hover:text-white transition-colors group relative"
|
||||
>
|
||||
<X size={18} />
|
||||
<span className="absolute -bottom-8 right-0 bg-neutral-800 text-neutral-200 text-xs rounded py-1 px-2 opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap">
|
||||
Close
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{agent.description && (
|
||||
<div className="mt-3 text-sm text-neutral-400 bg-neutral-800/30 p-3 rounded-md border border-neutral-800">
|
||||
{agent.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Chat content */}
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden p-3 bg-gradient-to-b from-neutral-900/50 to-neutral-950" ref={messagesContainerRef}>
|
||||
{isInitializing ? (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-emerald-500 to-emerald-700 flex items-center justify-center shadow-lg mb-4 animate-pulse">
|
||||
<Zap className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<p className="text-neutral-400 mb-2">Initializing connection...</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-bounce"
|
||||
style={{ animationDelay: '0ms' }}></span>
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-bounce"
|
||||
style={{ animationDelay: '150ms' }}></span>
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-bounce"
|
||||
style={{ animationDelay: '300ms' }}></span>
|
||||
</div>
|
||||
</div>
|
||||
) : messages.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center px-6">
|
||||
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-blue-500/20 to-emerald-500/20 flex items-center justify-center shadow-lg mb-5 border border-emerald-500/30">
|
||||
<MessageSquare className="h-6 w-6 text-emerald-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-neutral-300 mb-2">Start the conversation</h3>
|
||||
<p className="text-neutral-500 text-sm max-w-xs">
|
||||
Type a message below to begin chatting with {agent.name}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 w-full max-w-full">
|
||||
{messages.map((message) => {
|
||||
const messageContent = getMessageText(message);
|
||||
const agentColor = message.author === "user" ? "bg-emerald-500" : "bg-gradient-to-br from-neutral-800 to-neutral-900";
|
||||
const isExpanded = expandedFunctions[message.id] || false;
|
||||
|
||||
return (
|
||||
<ChatMessageComponent
|
||||
key={message.id}
|
||||
message={message}
|
||||
agentColor={agentColor}
|
||||
isExpanded={isExpanded}
|
||||
toggleExpansion={toggleFunctionExpansion}
|
||||
containsMarkdown={containsMarkdown}
|
||||
messageContent={messageContent}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{isSending && (
|
||||
<div className="flex justify-start">
|
||||
<div className="flex gap-3 max-w-[80%]">
|
||||
<Avatar
|
||||
className="bg-gradient-to-br from-purple-600 to-purple-800 shadow-md border-0"
|
||||
>
|
||||
<AvatarFallback className="bg-transparent">
|
||||
<Bot className="h-4 w-4 text-white" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="rounded-lg p-3 bg-gradient-to-br from-neutral-800 to-neutral-900 border border-neutral-700/50">
|
||||
<div className="flex space-x-2">
|
||||
<div className="h-2 w-2 rounded-full bg-emerald-400 animate-bounce"></div>
|
||||
<div className="h-2 w-2 rounded-full bg-emerald-400 animate-bounce [animation-delay:0.2s]"></div>
|
||||
<div className="h-2 w-2 rounded-full bg-emerald-400 animate-bounce [animation-delay:0.4s]"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Message input */}
|
||||
<div className="p-3 border-t border-neutral-800 bg-neutral-900">
|
||||
<ChatInput
|
||||
onSendMessage={handleSendMessageWithFiles}
|
||||
isLoading={isSending}
|
||||
placeholder="Type your message..."
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
// Use createPortal to render the modal directly to the document body
|
||||
return typeof document !== 'undefined'
|
||||
? createPortal(modalContent, document.body)
|
||||
: null;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/agent/styles.css │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
|
||||
.markdown-content {
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.markdown-content pre {
|
||||
white-space: pre-wrap;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.markdown-content code {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.markdown-content p {
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.markdown-content table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.markdown-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.overflow-wrap-anywhere {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/condition/ConditionDialog.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useState } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
import { ConditionType, ConditionTypeEnum } from "../../nodeFunctions";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Filter, ArrowRight } from "lucide-react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@/components/ui/select";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
const conditionTypes = [
|
||||
{
|
||||
id: "previous-output",
|
||||
name: "Previous output",
|
||||
description: "Validate the result returned by the previous node",
|
||||
icon: <Filter className="h-5 w-5 text-blue-400" />,
|
||||
color: "bg-blue-900/30 border-blue-700/50",
|
||||
},
|
||||
];
|
||||
|
||||
const operators = [
|
||||
{ value: "is_defined", label: "is defined" },
|
||||
{ value: "is_not_defined", label: "is not defined" },
|
||||
{ value: "equals", label: "is equal to" },
|
||||
{ value: "not_equals", label: "is not equal to" },
|
||||
{ value: "contains", label: "contains" },
|
||||
{ value: "not_contains", label: "does not contain" },
|
||||
{ value: "starts_with", label: "starts with" },
|
||||
{ value: "ends_with", label: "ends with" },
|
||||
{ value: "greater_than", label: "is greater than" },
|
||||
{ value: "greater_than_or_equal", label: "is greater than or equal to" },
|
||||
{ value: "less_than", label: "is less than" },
|
||||
{ value: "less_than_or_equal", label: "is less than or equal to" },
|
||||
{ value: "matches", label: "matches the regex" },
|
||||
{ value: "not_matches", label: "does not match the regex" },
|
||||
];
|
||||
|
||||
const outputFields = [
|
||||
{ value: "content", label: "Content" },
|
||||
{ value: "status", label: "Status" },
|
||||
];
|
||||
|
||||
function ConditionDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
selectedNode,
|
||||
handleUpdateNode,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
selectedNode: any;
|
||||
handleUpdateNode: any;
|
||||
}) {
|
||||
const [selectedType, setSelectedType] = useState("previous-output");
|
||||
const [selectedField, setSelectedField] = useState(outputFields[0].value);
|
||||
const [selectedOperator, setSelectedOperator] = useState(operators[0].value);
|
||||
const [comparisonValue, setComparisonValue] = useState("");
|
||||
|
||||
const handleConditionSave = (condition: ConditionType) => {
|
||||
const newConditions = selectedNode.data.conditions
|
||||
? [...selectedNode.data.conditions]
|
||||
: [];
|
||||
newConditions.push(condition);
|
||||
|
||||
const updatedNode = {
|
||||
...selectedNode,
|
||||
data: {
|
||||
...selectedNode.data,
|
||||
conditions: newConditions,
|
||||
},
|
||||
};
|
||||
|
||||
handleUpdateNode(updatedNode);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const getOperatorLabel = (value: string) => {
|
||||
return operators.find(op => op.value === value)?.label || value;
|
||||
};
|
||||
|
||||
const getFieldLabel = (value: string) => {
|
||||
return outputFields.find(field => field.value === value)?.label || value;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="bg-neutral-800 border-neutral-700 text-neutral-200 sm:max-w-[650px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New Condition</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-6 py-4">
|
||||
<div className="grid gap-4">
|
||||
<Label className="text-sm font-medium">Condition Type</Label>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{conditionTypes.map((type) => (
|
||||
<div
|
||||
key={type.id}
|
||||
className={`flex items-center space-x-3 rounded-md border p-3 cursor-pointer transition-all ${
|
||||
selectedType === type.id
|
||||
? "bg-blue-900/30 border-blue-600"
|
||||
: "border-neutral-700 hover:border-blue-700/50 hover:bg-neutral-700/50"
|
||||
}`}
|
||||
onClick={() => setSelectedType(type.id)}
|
||||
>
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-blue-900/40">
|
||||
{type.icon}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium">{type.name}</h4>
|
||||
<p className="text-xs text-neutral-400">{type.description}</p>
|
||||
</div>
|
||||
{selectedType === type.id && (
|
||||
<Badge className="bg-blue-600 text-neutral-100">Selected</Badge>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">Configuration</Label>
|
||||
{selectedType === "previous-output" && (
|
||||
<div className="flex items-center gap-2 text-sm text-neutral-400">
|
||||
<span>Output field</span>
|
||||
<ArrowRight className="h-3 w-3" />
|
||||
<span>Operator</span>
|
||||
{!["is_defined", "is_not_defined"].includes(selectedOperator) && (
|
||||
<>
|
||||
<ArrowRight className="h-3 w-3" />
|
||||
<span>Value</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedType === "previous-output" && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="field">Output Field</Label>
|
||||
<Select
|
||||
value={selectedField}
|
||||
onValueChange={setSelectedField}
|
||||
>
|
||||
<SelectTrigger id="field" className="bg-neutral-700 border-neutral-600">
|
||||
<SelectValue placeholder="Select field" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-neutral-700 border-neutral-600">
|
||||
{outputFields.map((field) => (
|
||||
<SelectItem key={field.value} value={field.value}>
|
||||
{field.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="operator">Operator</Label>
|
||||
<Select
|
||||
value={selectedOperator}
|
||||
onValueChange={setSelectedOperator}
|
||||
>
|
||||
<SelectTrigger id="operator" className="bg-neutral-700 border-neutral-600">
|
||||
<SelectValue placeholder="Select operator" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-neutral-700 border-neutral-600">
|
||||
{operators.map((op) => (
|
||||
<SelectItem key={op.value} value={op.value}>
|
||||
{op.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{!["is_defined", "is_not_defined"].includes(selectedOperator) && (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="value">Comparison Value</Label>
|
||||
<Input
|
||||
id="value"
|
||||
value={comparisonValue}
|
||||
onChange={(e) => setComparisonValue(e.target.value)}
|
||||
className="bg-neutral-700 border-neutral-600"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-md bg-neutral-700/50 border border-neutral-600 p-3 mt-4">
|
||||
<div className="text-sm font-medium text-neutral-400 mb-1">Preview</div>
|
||||
<div className="text-sm">
|
||||
<span className="text-blue-400 font-medium">{getFieldLabel(selectedField)}</span>
|
||||
{" "}
|
||||
<span className="text-neutral-300">{getOperatorLabel(selectedOperator)}</span>
|
||||
{" "}
|
||||
{!["is_defined", "is_not_defined"].includes(selectedOperator) && (
|
||||
<span className="text-emerald-400 font-medium">"{comparisonValue || "(empty)"}"</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="border-neutral-600 text-neutral-200 hover:bg-neutral-700"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleConditionSave({
|
||||
id: uuidv4(),
|
||||
type: ConditionTypeEnum.PREVIOUS_OUTPUT,
|
||||
data: {
|
||||
field: selectedField,
|
||||
operator: selectedOperator,
|
||||
value: comparisonValue,
|
||||
},
|
||||
});
|
||||
}}
|
||||
className="bg-blue-700 hover:bg-blue-600 text-white"
|
||||
>
|
||||
Add Condition
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { ConditionDialog };
|
||||
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/condition/ConditionForm.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
import { ConditionType, ConditionTypeEnum } from "../../nodeFunctions";
|
||||
import { ConditionDialog } from "./ConditionDialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Filter, Trash2, Plus } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
function ConditionForm({
|
||||
selectedNode,
|
||||
handleUpdateNode,
|
||||
}: {
|
||||
selectedNode: any;
|
||||
handleUpdateNode: any;
|
||||
setEdges: any;
|
||||
setIsOpen: any;
|
||||
setSelectedNode: any;
|
||||
}) {
|
||||
const [node, setNode] = useState(selectedNode);
|
||||
|
||||
const [conditions, setConditions] = useState<ConditionType[]>(
|
||||
selectedNode.data.conditions || []
|
||||
);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [deleteDialog, setDeleteDialog] = useState(false);
|
||||
const [conditionToDelete, setConditionToDelete] =
|
||||
useState<ConditionType | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedNode) {
|
||||
setNode(selectedNode);
|
||||
setConditions(selectedNode.data.conditions || []);
|
||||
}
|
||||
}, [selectedNode]);
|
||||
|
||||
const handleDelete = (condition: ConditionType) => {
|
||||
setConditionToDelete(condition);
|
||||
setDeleteDialog(true);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
if (!conditionToDelete) return;
|
||||
|
||||
const newConditions = conditions.filter(
|
||||
(c) => c.id !== conditionToDelete.id
|
||||
);
|
||||
setConditions(newConditions);
|
||||
handleUpdateNode({
|
||||
...node,
|
||||
data: { ...node.data, conditions: newConditions },
|
||||
});
|
||||
setDeleteDialog(false);
|
||||
setConditionToDelete(null);
|
||||
};
|
||||
|
||||
const renderCondition = (condition: ConditionType) => {
|
||||
if (condition.type === ConditionTypeEnum.PREVIOUS_OUTPUT) {
|
||||
type OperatorType =
|
||||
| "is_defined"
|
||||
| "is_not_defined"
|
||||
| "equals"
|
||||
| "not_equals"
|
||||
| "contains"
|
||||
| "not_contains"
|
||||
| "starts_with"
|
||||
| "ends_with"
|
||||
| "greater_than"
|
||||
| "greater_than_or_equal"
|
||||
| "less_than"
|
||||
| "less_than_or_equal"
|
||||
| "matches"
|
||||
| "not_matches";
|
||||
|
||||
const operatorText: Record<OperatorType, string> = {
|
||||
is_defined: "is defined",
|
||||
is_not_defined: "is not defined",
|
||||
equals: "is equal to",
|
||||
not_equals: "is not equal to",
|
||||
contains: "contains",
|
||||
not_contains: "does not contain",
|
||||
starts_with: "starts with",
|
||||
ends_with: "ends with",
|
||||
greater_than: "is greater than",
|
||||
greater_than_or_equal: "is greater than or equal to",
|
||||
less_than: "is less than",
|
||||
less_than_or_equal: "is less than or equal to",
|
||||
matches: "matches the regex",
|
||||
not_matches: "does not match the regex",
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={condition.id}
|
||||
className="p-3 rounded-md cursor-pointer transition-colors bg-neutral-800 hover:bg-neutral-700 border border-neutral-700 mb-2 group"
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="bg-blue-900/50 rounded-full p-1.5 flex-shrink-0">
|
||||
<Filter size={18} className="text-blue-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium text-neutral-200">Condition</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDelete(condition)}
|
||||
className="h-7 w-7 text-neutral-400 opacity-0 group-hover:opacity-100 hover:text-red-500 hover:bg-red-900/20"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs bg-blue-900/20 text-blue-400 border-blue-700/50"
|
||||
>
|
||||
Field
|
||||
</Badge>
|
||||
<span className="text-sm text-neutral-300 font-medium">{condition.data.field}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-1 mt-1.5">
|
||||
<span className="text-sm text-neutral-400">{operatorText[condition.data.operator as OperatorType]}</span>
|
||||
{!["is_defined", "is_not_defined"].includes(condition.data.operator) && (
|
||||
<span className="text-sm font-medium text-emerald-400">
|
||||
"{condition.data.value}"
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="p-4 border-b border-neutral-700 flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-md font-medium text-neutral-200">Logic Type</h3>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs bg-blue-900/20 text-blue-400 border-blue-700/50"
|
||||
>
|
||||
{node.data.type === "or" ? "ANY" : "ALL"}
|
||||
</Badge>
|
||||
</div>
|
||||
<Select
|
||||
value={node.data.type || "and"}
|
||||
onValueChange={(value) => {
|
||||
const updatedNode = {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
type: value,
|
||||
},
|
||||
};
|
||||
setNode(updatedNode);
|
||||
handleUpdateNode(updatedNode);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[120px] h-8 bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||
<SelectValue placeholder="Select type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||
<SelectItem value="and">ALL (AND)</SelectItem>
|
||||
<SelectItem value="or">ANY (OR)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<p className="text-sm text-neutral-400 mt-2">
|
||||
{node.data.type === "or"
|
||||
? "Any of the following conditions must be true to proceed."
|
||||
: "All of the following conditions must be true to proceed."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-4 py-4 min-h-0">
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-md font-medium text-neutral-200">Conditions</h3>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 bg-blue-800/20 hover:bg-blue-700/30 border-blue-700/50 text-blue-300"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Plus size={14} className="mr-1" />
|
||||
Add Condition
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{conditions.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{conditions.map((condition) => renderCondition(condition))}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
onClick={() => setOpen(true)}
|
||||
className="flex flex-col items-center justify-center p-6 rounded-lg border-2 border-dashed border-neutral-700 hover:border-blue-600/50 hover:bg-neutral-800/50 transition-colors cursor-pointer text-center"
|
||||
>
|
||||
<Filter className="h-10 w-10 text-neutral-500 mb-2" />
|
||||
<p className="text-neutral-400">No conditions yet</p>
|
||||
<p className="text-sm text-neutral-500 mt-1">Click to add a condition</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConditionDialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
selectedNode={selectedNode}
|
||||
handleUpdateNode={handleUpdateNode}
|
||||
/>
|
||||
|
||||
<Dialog open={deleteDialog} onOpenChange={setDeleteDialog}>
|
||||
<DialogContent className="bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Confirm Delete</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<p>Are you sure you want to delete this condition?</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-neutral-600 text-neutral-300 hover:bg-neutral-700"
|
||||
onClick={() => {
|
||||
setDeleteDialog(false);
|
||||
setConditionToDelete(null);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="bg-red-900 hover:bg-red-800 text-white"
|
||||
onClick={confirmDelete}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ConditionForm };
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/condition/ConditionNode.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Handle, Node, NodeProps, Position, useEdges } from "@xyflow/react";
|
||||
import { FilterIcon, ArrowRight } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { BaseNode } from "../../BaseNode";
|
||||
import { ConditionType, ConditionTypeEnum } from "../../nodeFunctions";
|
||||
|
||||
export type ConditionNodeType = Node<
|
||||
{
|
||||
label?: string;
|
||||
type?: "and" | "or";
|
||||
conditions?: ConditionType[];
|
||||
},
|
||||
"condition-node"
|
||||
>;
|
||||
|
||||
export type OperatorType =
|
||||
| "is_defined"
|
||||
| "is_not_defined"
|
||||
| "equals"
|
||||
| "not_equals"
|
||||
| "contains"
|
||||
| "not_contains"
|
||||
| "starts_with"
|
||||
| "ends_with"
|
||||
| "greater_than"
|
||||
| "greater_than_or_equal"
|
||||
| "less_than"
|
||||
| "less_than_or_equal"
|
||||
| "matches"
|
||||
| "not_matches";
|
||||
|
||||
const operatorText: Record<OperatorType, string> = {
|
||||
equals: "is equal to",
|
||||
not_equals: "is not equal to",
|
||||
contains: "contains",
|
||||
not_contains: "does not contain",
|
||||
starts_with: "starts with",
|
||||
ends_with: "ends with",
|
||||
greater_than: "is greater than",
|
||||
greater_than_or_equal: "is greater than or equal to",
|
||||
less_than: "is less than",
|
||||
less_than_or_equal: "is less than or equal to",
|
||||
matches: "matches the pattern",
|
||||
not_matches: "does not match the pattern",
|
||||
is_defined: "is defined",
|
||||
is_not_defined: "is not defined",
|
||||
};
|
||||
|
||||
export function ConditionNode(props: NodeProps) {
|
||||
const { selected, data } = props;
|
||||
const edges = useEdges();
|
||||
const isExecuting = data.isExecuting as boolean | undefined;
|
||||
|
||||
const typeText = {
|
||||
and: "all of the following conditions",
|
||||
or: "any of the following conditions",
|
||||
};
|
||||
|
||||
const isHandleConnected = (handleId: string) => {
|
||||
return edges.some(
|
||||
(edge) => edge.source === props.id && edge.sourceHandle === handleId,
|
||||
);
|
||||
};
|
||||
|
||||
const isBottomHandleConnected = isHandleConnected("bottom-handle");
|
||||
|
||||
const conditions: ConditionType[] = data.conditions as ConditionType[];
|
||||
// const statistics: StatisticType = data.statistics as StatisticType;
|
||||
|
||||
const renderCondition = (condition: ConditionType) => {
|
||||
const isConnected = isHandleConnected(condition.id);
|
||||
|
||||
if (condition.type === ConditionTypeEnum.PREVIOUS_OUTPUT) {
|
||||
return (
|
||||
<div
|
||||
className="mb-3 cursor-pointer rounded-lg border border-purple-700/40 bg-purple-950/10 p-3 text-left transition-all duration-200 hover:border-purple-600/50 hover:bg-purple-900/10"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-neutral-300">
|
||||
O campo{" "}
|
||||
<span className="font-semibold text-purple-400">
|
||||
{condition.data.field}
|
||||
</span>{" "}
|
||||
<span className="text-neutral-400">
|
||||
{operatorText[condition.data.operator as OperatorType]}
|
||||
</span>{" "}
|
||||
{!["is_defined", "is_not_defined"].includes(
|
||||
condition.data.operator,
|
||||
) && (
|
||||
<span className="font-semibold text-green-400">
|
||||
"{condition.data.value}"
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Handle
|
||||
className={cn(
|
||||
"!rounded-full transition-all duration-300",
|
||||
isConnected ? "!bg-purple-500 !border-purple-400" : "!bg-neutral-400 !border-neutral-500"
|
||||
)}
|
||||
style={{
|
||||
top: "50%",
|
||||
right: "-5px",
|
||||
transform: "translateY(-50%)",
|
||||
height: "14px",
|
||||
position: "relative",
|
||||
width: "14px",
|
||||
}}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id={condition.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseNode hasTarget={true} selected={selected || false} borderColor="purple" isExecuting={isExecuting}>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-purple-900/40 shadow-sm">
|
||||
<FilterIcon className="h-5 w-5 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-medium text-purple-400">
|
||||
{data.label as string}
|
||||
</p>
|
||||
<p className="text-xs text-neutral-400">
|
||||
Matches {typeText[(data.type as "and" | "or") || "and"]}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{conditions && conditions.length > 0 && Array.isArray(conditions) ? (
|
||||
conditions.map((condition) => (
|
||||
<div key={condition.id}>{renderCondition(condition)}</div>
|
||||
))
|
||||
) : (
|
||||
<div className="mb-3 flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-purple-700/40 bg-purple-950/10 p-5 text-center transition-all duration-200 hover:border-purple-600/50 hover:bg-purple-900/20">
|
||||
<FilterIcon className="h-8 w-8 text-purple-700/50 mb-2" />
|
||||
<p className="text-purple-400">No conditions configured</p>
|
||||
<p className="mt-1 text-xs text-neutral-500">Click to add a condition</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 flex items-center justify-end text-sm text-neutral-400 transition-colors">
|
||||
<div className="flex items-center space-x-1 rounded-md py-1 px-2">
|
||||
<span>Next step</span>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<Handle
|
||||
className={cn(
|
||||
"!w-3 !h-3 !rounded-full transition-all duration-300",
|
||||
isBottomHandleConnected ? "!bg-purple-500 !border-purple-400" : "!bg-neutral-400 !border-neutral-500",
|
||||
selected && isBottomHandleConnected && "!bg-purple-400 !border-purple-300"
|
||||
)}
|
||||
style={{
|
||||
right: "0px",
|
||||
top: "calc(100% - 25px)",
|
||||
}}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="bottom-handle"
|
||||
/>
|
||||
</div>
|
||||
</BaseNode>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @author: Victor Calazans - Implementation of Delay node form │
|
||||
│ @file: /app/agents/workflows/nodes/components/delay/DelayForm.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Delay form developed by: Victor Calazans │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Delay form implementation date: May 17, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useState, useEffect } from "react";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { Clock, Trash2, Save, AlertCircle, HourglassIcon } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
import { DelayType, DelayUnitEnum } from "../../nodeFunctions";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function DelayForm({
|
||||
selectedNode,
|
||||
handleUpdateNode,
|
||||
setEdges,
|
||||
setIsOpen,
|
||||
setSelectedNode,
|
||||
}: {
|
||||
selectedNode: any;
|
||||
handleUpdateNode: (node: any) => void;
|
||||
setEdges: any;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setSelectedNode: Dispatch<SetStateAction<any>>;
|
||||
}) {
|
||||
const [delay, setDelay] = useState<DelayType>({
|
||||
value: 1,
|
||||
unit: DelayUnitEnum.SECONDS,
|
||||
description: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedNode?.data?.delay) {
|
||||
setDelay(selectedNode.data.delay);
|
||||
}
|
||||
}, [selectedNode]);
|
||||
|
||||
const handleSave = () => {
|
||||
handleUpdateNode({
|
||||
...selectedNode,
|
||||
data: {
|
||||
...selectedNode.data,
|
||||
delay,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setEdges((edges: any) => {
|
||||
return edges.filter(
|
||||
(edge: any) => edge.source !== selectedNode.id && edge.target !== selectedNode.id
|
||||
);
|
||||
});
|
||||
|
||||
setIsOpen(false);
|
||||
setSelectedNode(null);
|
||||
};
|
||||
|
||||
const getUnitLabel = (unit: DelayUnitEnum) => {
|
||||
const units = {
|
||||
[DelayUnitEnum.SECONDS]: "Seconds",
|
||||
[DelayUnitEnum.MINUTES]: "Minutes",
|
||||
[DelayUnitEnum.HOURS]: "Hours",
|
||||
[DelayUnitEnum.DAYS]: "Days",
|
||||
};
|
||||
return units[unit] || unit;
|
||||
};
|
||||
|
||||
const getTimeDescription = () => {
|
||||
const value = delay.value || 0;
|
||||
|
||||
if (value <= 0) return "Invalid time";
|
||||
|
||||
if (value === 1) {
|
||||
return `1 ${getUnitLabel(delay.unit).slice(0, -1)}`;
|
||||
}
|
||||
|
||||
return `${value} ${getUnitLabel(delay.unit)}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="p-4 border-b border-neutral-700 flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-md font-medium text-neutral-200">Delay Duration</h3>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs bg-yellow-900/20 text-yellow-400 border-yellow-700/50"
|
||||
>
|
||||
{getTimeDescription().toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
<Select
|
||||
value={delay.unit}
|
||||
onValueChange={(value) =>
|
||||
setDelay({
|
||||
...delay,
|
||||
unit: value as DelayUnitEnum,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[120px] h-8 bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||
<SelectValue placeholder="Unit" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||
<SelectItem value={DelayUnitEnum.SECONDS}>Seconds</SelectItem>
|
||||
<SelectItem value={DelayUnitEnum.MINUTES}>Minutes</SelectItem>
|
||||
<SelectItem value={DelayUnitEnum.HOURS}>Hours</SelectItem>
|
||||
<SelectItem value={DelayUnitEnum.DAYS}>Days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 min-h-0">
|
||||
<div className="grid gap-4">
|
||||
<div className="p-3 rounded-md bg-yellow-900/10 border border-yellow-700/30 mb-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="bg-yellow-900/50 rounded-full p-1.5 flex-shrink-0">
|
||||
<Clock size={18} className="text-yellow-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium text-neutral-200">Time Delay</h3>
|
||||
<p className="text-sm text-neutral-400 mt-1">
|
||||
Pause workflow execution for a specified amount of time
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="delay-value">Delay Value</Label>
|
||||
<Input
|
||||
id="delay-value"
|
||||
type="number"
|
||||
min="1"
|
||||
className="bg-neutral-700 border-neutral-600"
|
||||
value={delay.value}
|
||||
onChange={(e) =>
|
||||
setDelay({
|
||||
...delay,
|
||||
value: parseInt(e.target.value) || 1,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="delay-description">Description (optional)</Label>
|
||||
<Textarea
|
||||
id="delay-description"
|
||||
className="bg-neutral-700 border-neutral-600 min-h-[100px] resize-none"
|
||||
value={delay.description}
|
||||
onChange={(e) =>
|
||||
setDelay({
|
||||
...delay,
|
||||
description: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="Add a description for this delay"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{delay.value > 0 ? (
|
||||
<div className="rounded-md bg-neutral-700/50 border border-neutral-600 p-3 mt-2">
|
||||
<div className="text-sm font-medium text-neutral-400 mb-1">Preview</div>
|
||||
<div className="flex items-start gap-2 p-2 rounded-md bg-neutral-800/70">
|
||||
<div className="rounded-full bg-yellow-900/30 p-1.5 mt-0.5">
|
||||
<HourglassIcon size={15} className="text-yellow-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm text-yellow-400 font-medium">
|
||||
{getTimeDescription()} delay
|
||||
</span>
|
||||
{delay.description && (
|
||||
<span className="text-xs text-neutral-400 mt-1">
|
||||
{delay.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md bg-neutral-700/30 border border-neutral-600/50 p-4 flex flex-col items-center justify-center text-center">
|
||||
<AlertCircle className="h-6 w-6 text-neutral-500 mb-2" />
|
||||
<p className="text-neutral-400 text-sm">Please set a valid delay time</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-neutral-700 flex-shrink-0">
|
||||
<div className="flex gap-2 justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-red-700/50 bg-red-900/20 text-red-400 hover:bg-red-900/30 hover:text-red-300"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete Node
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-yellow-700 hover:bg-yellow-600 text-white flex items-center gap-2"
|
||||
onClick={handleSave}
|
||||
>
|
||||
<Save size={16} />
|
||||
Save Delay
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @author: Victor Calazans - Implementation of Delay node functionality │
|
||||
│ @file: /app/agents/workflows/nodes/components/delay/DelayNode.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Delay node developed by: Victor Calazans │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Delay node implementation date: May 17, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Handle, NodeProps, Position, useEdges } from "@xyflow/react";
|
||||
import { Clock, ArrowRight, Timer } from "lucide-react";
|
||||
import { DelayType } from "../../nodeFunctions";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { BaseNode } from "../../BaseNode";
|
||||
|
||||
export function DelayNode(props: NodeProps) {
|
||||
const { selected, data } = props;
|
||||
|
||||
const edges = useEdges();
|
||||
const isExecuting = data.isExecuting as boolean | undefined;
|
||||
|
||||
const isHandleConnected = (handleId: string) => {
|
||||
return edges.some(
|
||||
(edge) => edge.source === props.id && edge.sourceHandle === handleId
|
||||
);
|
||||
};
|
||||
|
||||
const isBottomHandleConnected = isHandleConnected("bottom-handle");
|
||||
|
||||
const delay = data.delay as DelayType | undefined;
|
||||
|
||||
const getUnitLabel = (unit: string) => {
|
||||
switch (unit) {
|
||||
case 'seconds':
|
||||
return 'Seconds';
|
||||
case 'minutes':
|
||||
return 'Minutes';
|
||||
case 'hours':
|
||||
return 'Hours';
|
||||
case 'days':
|
||||
return 'Days';
|
||||
default:
|
||||
return unit;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseNode hasTarget={true} selected={selected || false} borderColor="yellow" isExecuting={isExecuting}>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-yellow-900/40 shadow-sm">
|
||||
<Clock className="h-5 w-5 text-yellow-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-medium text-yellow-400">
|
||||
{data.label as string}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{delay ? (
|
||||
<div className="mb-3 rounded-lg border border-yellow-700/40 bg-yellow-950/10 p-3 transition-all duration-200 hover:border-yellow-600/50 hover:bg-yellow-900/10">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center">
|
||||
<Timer className="h-4 w-4 text-yellow-400" />
|
||||
<span className="ml-1.5 font-medium text-white">Delay</span>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="px-1.5 py-0 text-xs bg-yellow-900/30 text-yellow-400 border-yellow-700/40"
|
||||
>
|
||||
{getUnitLabel(delay.unit)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center">
|
||||
<span className="text-lg font-semibold text-yellow-300">{delay.value}</span>
|
||||
<span className="ml-1 text-sm text-neutral-400">{delay.unit}</span>
|
||||
</div>
|
||||
|
||||
{delay.description && (
|
||||
<p className="mt-2 text-xs text-neutral-400 line-clamp-2">
|
||||
{delay.description.slice(0, 80)} {delay.description.length > 80 && '...'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-3 flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-yellow-700/40 bg-yellow-950/10 p-5 text-center transition-all duration-200 hover:border-yellow-600/50 hover:bg-yellow-900/20">
|
||||
<Clock className="h-8 w-8 text-yellow-700/50 mb-2" />
|
||||
<p className="text-yellow-400">No delay configured</p>
|
||||
<p className="mt-1 text-xs text-neutral-500">Click to configure</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 flex items-center justify-end text-sm text-neutral-400 transition-colors">
|
||||
<div className="flex items-center space-x-1 rounded-md py-1 px-2">
|
||||
<span>Next step</span>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<Handle
|
||||
className={cn(
|
||||
"!w-3 !h-3 !rounded-full transition-all duration-300",
|
||||
isBottomHandleConnected ? "!bg-yellow-500 !border-yellow-400" : "!bg-neutral-400 !border-neutral-500",
|
||||
selected && isBottomHandleConnected && "!bg-yellow-400 !border-yellow-300"
|
||||
)}
|
||||
style={{
|
||||
right: "-8px",
|
||||
top: "calc(100% - 25px)",
|
||||
}}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="bottom-handle"
|
||||
/>
|
||||
</div>
|
||||
</BaseNode>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/message/MessageForm.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* eslint-disable jsx-a11y/alt-text */
|
||||
import { useEdges, useNodes } from "@xyflow/react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { listAgents } from "@/services/agentService";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
MessageSquare,
|
||||
Save,
|
||||
Text,
|
||||
Image,
|
||||
File,
|
||||
Video,
|
||||
AlertCircle
|
||||
} from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
function MessageForm({
|
||||
selectedNode,
|
||||
handleUpdateNode,
|
||||
setEdges,
|
||||
setIsOpen,
|
||||
setSelectedNode,
|
||||
}: {
|
||||
selectedNode: any;
|
||||
handleUpdateNode: any;
|
||||
setEdges: any;
|
||||
setIsOpen: any;
|
||||
setSelectedNode: any;
|
||||
}) {
|
||||
const [node, setNode] = useState(selectedNode);
|
||||
const [messageType, setMessageType] = useState("text");
|
||||
const [content, setContent] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [allAgents, setAllAgents] = useState<Agent[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const edges = useEdges();
|
||||
const nodes = useNodes();
|
||||
|
||||
const connectedNode = useMemo(() => {
|
||||
const edge = edges.find((edge) => edge.source === selectedNode.id);
|
||||
if (!edge) return null;
|
||||
const node = nodes.find((node) => node.id === edge.target);
|
||||
return node || null;
|
||||
}, [edges, nodes, selectedNode.id]);
|
||||
|
||||
const user = typeof window !== "undefined" ? JSON.parse(localStorage.getItem("user") || '{}') : {};
|
||||
const clientId = user?.client_id || "";
|
||||
|
||||
const currentAgent = typeof window !== "undefined" ?
|
||||
JSON.parse(localStorage.getItem("current_workflow_agent") || '{}') : {};
|
||||
const currentAgentId = currentAgent?.id;
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedNode) {
|
||||
setNode(selectedNode);
|
||||
setMessageType(selectedNode.data.message?.type || "text");
|
||||
setContent(selectedNode.data.message?.content || "");
|
||||
}
|
||||
}, [selectedNode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clientId) return;
|
||||
setLoading(true);
|
||||
listAgents(clientId)
|
||||
.then((res) => {
|
||||
const filteredAgents = res.data.filter((agent: Agent) => agent.id !== currentAgentId);
|
||||
setAllAgents(filteredAgents);
|
||||
setAgents(filteredAgents);
|
||||
})
|
||||
.catch((error) => console.error("Error loading agents:", error))
|
||||
.finally(() => setLoading(false));
|
||||
}, [clientId, currentAgentId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchTerm.trim() === "") {
|
||||
setAgents(allAgents);
|
||||
} else {
|
||||
const filtered = allAgents.filter((agent) =>
|
||||
agent.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
setAgents(filtered);
|
||||
}
|
||||
}, [searchTerm, allAgents]);
|
||||
|
||||
const handleDeleteEdge = useCallback(() => {
|
||||
const id = edges.find((edge: any) => edge.source === selectedNode.id)?.id;
|
||||
setEdges((edges: any) => {
|
||||
const left = edges.filter((edge: any) => edge.id !== id);
|
||||
return left;
|
||||
});
|
||||
}, [nodes, edges, selectedNode, setEdges]);
|
||||
|
||||
const handleSelectAgent = (agent: Agent) => {
|
||||
const updatedNode = {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
agent,
|
||||
},
|
||||
};
|
||||
setNode(updatedNode);
|
||||
handleUpdateNode(updatedNode);
|
||||
};
|
||||
|
||||
const getAgentTypeName = (type: string) => {
|
||||
const agentTypes: Record<string, string> = {
|
||||
llm: "LLM Agent",
|
||||
a2a: "A2A Agent",
|
||||
sequential: "Sequential Agent",
|
||||
parallel: "Parallel Agent",
|
||||
loop: "Loop Agent",
|
||||
workflow: "Workflow Agent",
|
||||
task: "Task Agent",
|
||||
};
|
||||
return agentTypes[type] || type;
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const updatedNode = {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
message: {
|
||||
type: messageType,
|
||||
content,
|
||||
},
|
||||
},
|
||||
};
|
||||
setNode(updatedNode);
|
||||
handleUpdateNode(updatedNode);
|
||||
};
|
||||
|
||||
const messageTypeInfo = {
|
||||
text: {
|
||||
icon: <Text className="h-5 w-5 text-orange-400" />,
|
||||
name: "Text Message",
|
||||
description: "Simple text message",
|
||||
color: "bg-orange-900/30 border-orange-700/50",
|
||||
},
|
||||
image: {
|
||||
icon: <Image className="h-5 w-5 text-blue-400" />,
|
||||
name: "Image",
|
||||
description: "Image URL or base64",
|
||||
color: "bg-blue-900/30 border-blue-700/50",
|
||||
},
|
||||
file: {
|
||||
icon: <File className="h-5 w-5 text-emerald-400" />,
|
||||
name: "File",
|
||||
description: "File URL or base64",
|
||||
color: "bg-emerald-900/30 border-emerald-700/50",
|
||||
},
|
||||
video: {
|
||||
icon: <Video className="h-5 w-5 text-purple-400" />,
|
||||
name: "Video",
|
||||
description: "Video URL or base64",
|
||||
color: "bg-purple-900/30 border-purple-700/50",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="p-4 border-b border-neutral-700 flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-md font-medium text-neutral-200">Message Type</h3>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs bg-orange-900/20 text-orange-400 border-orange-700/50"
|
||||
>
|
||||
{messageType === "text" ? "TEXT" : messageType.toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
<Select
|
||||
value={messageType}
|
||||
onValueChange={setMessageType}
|
||||
>
|
||||
<SelectTrigger className="w-[120px] h-8 bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||
<SelectValue placeholder="Select type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||
<SelectItem value="text">Text</SelectItem>
|
||||
{/* Other options can be enabled in the future */}
|
||||
{/* <SelectItem value="image">Image</SelectItem>
|
||||
<SelectItem value="file">File</SelectItem>
|
||||
<SelectItem value="video">Video</SelectItem> */}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 min-h-0">
|
||||
<div className="grid gap-4">
|
||||
<div className="p-3 rounded-md bg-orange-900/10 border border-orange-700/30 mb-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="bg-orange-900/50 rounded-full p-1.5 flex-shrink-0">
|
||||
<MessageSquare size={18} className="text-orange-300" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium text-neutral-200">{messageTypeInfo.text.name}</h3>
|
||||
<p className="text-sm text-neutral-400 mt-1">{messageTypeInfo.text.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="content">Message Content</Label>
|
||||
<Textarea
|
||||
id="content"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder="Type your message here..."
|
||||
className="min-h-[150px] bg-neutral-700 border-neutral-600 resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{content.trim() !== "" ? (
|
||||
<div className="rounded-md bg-neutral-700/50 border border-neutral-600 p-3 mt-2">
|
||||
<div className="text-sm font-medium text-neutral-400 mb-1">Preview</div>
|
||||
<div className="flex items-start gap-2 p-2 rounded-md bg-neutral-800/70">
|
||||
<div className="rounded-full bg-orange-900/30 p-1.5 mt-0.5">
|
||||
<MessageSquare size={15} className="text-orange-400" />
|
||||
</div>
|
||||
<div className="text-sm text-neutral-300 whitespace-pre-wrap">{content}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md bg-neutral-700/30 border border-neutral-600/50 p-4 flex flex-col items-center justify-center text-center">
|
||||
<AlertCircle className="h-6 w-6 text-neutral-500 mb-2" />
|
||||
<p className="text-neutral-400 text-sm">Your message will appear here</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-neutral-700 flex-shrink-0">
|
||||
<Button
|
||||
className="w-full bg-orange-700 hover:bg-orange-600 text-white flex items-center gap-2 justify-center"
|
||||
onClick={handleSave}
|
||||
>
|
||||
<Save size={16} />
|
||||
Save Message
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { MessageForm };
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/message/MessageNode.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Handle, NodeProps, Position, useEdges } from "@xyflow/react";
|
||||
import { MessageCircle, Text, Image, File, Video, ArrowRight } from "lucide-react";
|
||||
import { MessageType, MessageTypeEnum } from "../../nodeFunctions";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { BaseNode } from "../../BaseNode";
|
||||
|
||||
export function MessageNode(props: NodeProps) {
|
||||
const { selected, data } = props;
|
||||
const edges = useEdges();
|
||||
const isExecuting = data.isExecuting as boolean | undefined;
|
||||
|
||||
const isHandleConnected = (handleId: string) => {
|
||||
return edges.some(
|
||||
(edge) => edge.source === props.id && edge.sourceHandle === handleId
|
||||
);
|
||||
};
|
||||
|
||||
const isBottomHandleConnected = isHandleConnected("bottom-handle");
|
||||
|
||||
const message = data.message as MessageType | undefined;
|
||||
|
||||
const getMessageTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case MessageTypeEnum.TEXT:
|
||||
return <Text className="h-4 w-4 text-orange-400" />;
|
||||
case "image":
|
||||
return <Image className="h-4 w-4 text-blue-400" />;
|
||||
case "file":
|
||||
return <File className="h-4 w-4 text-emerald-400" />;
|
||||
case "video":
|
||||
return <Video className="h-4 w-4 text-purple-400" />;
|
||||
default:
|
||||
return <MessageCircle className="h-4 w-4 text-orange-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getMessageTypeColor = (type: string) => {
|
||||
switch (type) {
|
||||
case MessageTypeEnum.TEXT:
|
||||
return 'bg-orange-900/30 text-orange-400 border-orange-700/40';
|
||||
case "image":
|
||||
return 'bg-blue-900/30 text-blue-400 border-blue-700/40';
|
||||
case "file":
|
||||
return 'bg-emerald-900/30 text-emerald-400 border-emerald-700/40';
|
||||
case "video":
|
||||
return 'bg-purple-900/30 text-purple-400 border-purple-700/40';
|
||||
default:
|
||||
return 'bg-orange-900/30 text-orange-400 border-orange-700/40';
|
||||
}
|
||||
};
|
||||
|
||||
const getMessageTypeName = (type: string) => {
|
||||
const messageTypes: Record<string, string> = {
|
||||
text: "Text Message",
|
||||
image: "Image",
|
||||
file: "File",
|
||||
video: "Video",
|
||||
};
|
||||
return messageTypes[type] || type;
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseNode hasTarget={true} selected={selected || false} borderColor="orange" isExecuting={isExecuting}>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-orange-900/40 shadow-sm">
|
||||
<MessageCircle className="h-5 w-5 text-orange-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-medium text-orange-400">
|
||||
{data.label as string}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message ? (
|
||||
<div className="mb-3 rounded-lg border border-orange-700/40 bg-orange-950/10 p-3 transition-all duration-200 hover:border-orange-600/50 hover:bg-orange-900/10">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center">
|
||||
{getMessageTypeIcon(message.type)}
|
||||
<span className="ml-1.5 font-medium text-white">{getMessageTypeName(message.type)}</span>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn("px-1.5 py-0 text-xs", getMessageTypeColor(message.type))}
|
||||
>
|
||||
{message.type}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{message.content && (
|
||||
<p className="mt-2 text-xs text-neutral-400 line-clamp-2">
|
||||
{message.content.slice(0, 80)} {message.content.length > 80 && '...'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-3 flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-orange-700/40 bg-orange-950/10 p-5 text-center transition-all duration-200 hover:border-orange-600/50 hover:bg-orange-900/20">
|
||||
<MessageCircle className="h-8 w-8 text-orange-700/50 mb-2" />
|
||||
<p className="text-orange-400">No message configured</p>
|
||||
<p className="mt-1 text-xs text-neutral-500">Click to configure</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 flex items-center justify-end text-sm text-neutral-400 transition-colors">
|
||||
<div className="flex items-center space-x-1 rounded-md py-1 px-2">
|
||||
<span>Next step</span>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<Handle
|
||||
className={cn(
|
||||
"!w-3 !h-3 !rounded-full transition-all duration-300",
|
||||
isBottomHandleConnected ? "!bg-orange-500 !border-orange-400" : "!bg-neutral-400 !border-neutral-500",
|
||||
selected && isBottomHandleConnected && "!bg-orange-400 !border-orange-300"
|
||||
)}
|
||||
style={{
|
||||
right: "-8px",
|
||||
top: "calc(100% - 25px)",
|
||||
}}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="bottom-handle"
|
||||
/>
|
||||
</div>
|
||||
</BaseNode>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/components/start/StartNode.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Handle, Node, NodeProps, Position, useEdges } from "@xyflow/react";
|
||||
import { Zap, ArrowRight } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { BaseNode } from "../../BaseNode";
|
||||
|
||||
export type StartNodeType = Node<
|
||||
{
|
||||
label?: string;
|
||||
},
|
||||
"start-node"
|
||||
>;
|
||||
|
||||
export function StartNode(props: NodeProps) {
|
||||
const { selected, data } = props;
|
||||
const edges = useEdges();
|
||||
const isExecuting = data.isExecuting as boolean | undefined;
|
||||
|
||||
const isSourceHandleConnected = edges.some(
|
||||
(edge) => edge.source === props.id
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseNode hasTarget={true} selected={selected || false} borderColor="emerald" isExecuting={isExecuting}>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-emerald-900/40 shadow-sm">
|
||||
<Zap className="h-5 w-5 text-emerald-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-medium text-emerald-400">Start</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3 rounded-lg border border-emerald-700/40 bg-emerald-950/10 p-3 transition-all duration-200 hover:border-emerald-600/50 hover:bg-emerald-900/10">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center">
|
||||
<span className="font-medium text-white">Input: User content</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-2 text-xs text-neutral-400">
|
||||
The workflow begins when a user sends a message to the agent
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center justify-end text-sm text-neutral-400 transition-colors">
|
||||
<div className="flex items-center space-x-1 rounded-md py-1 px-2">
|
||||
<span>Next step</span>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<Handle
|
||||
className={cn(
|
||||
"!w-3 !h-3 !rounded-full transition-all duration-300",
|
||||
isSourceHandleConnected ? "!bg-emerald-500 !border-emerald-400" : "!bg-neutral-400 !border-neutral-500",
|
||||
selected && isSourceHandleConnected && "!bg-emerald-400 !border-emerald-300"
|
||||
)}
|
||||
style={{
|
||||
right: "-8px",
|
||||
top: "calc(100% - 25px)",
|
||||
}}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
/>
|
||||
</div>
|
||||
</BaseNode>
|
||||
);
|
||||
}
|
||||
109
frontend/app/agents/workflows/nodes/index.ts
Normal file
109
frontend/app/agents/workflows/nodes/index.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @author: Victor Calazans - Implementation of Delay node type │
|
||||
│ @file: /app/agents/workflows/nodes/index.ts │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Delay node functionality developed by: Victor Calazans │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Delay implementation date: May 17, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
import type { NodeTypes, BuiltInNode, Node } from "@xyflow/react";
|
||||
|
||||
import { ConditionNode } from "./components/condition/ConditionNode";
|
||||
import { AgentNode } from "./components/agent/AgentNode";
|
||||
import { StartNode, StartNodeType } from "./components/start/StartNode";
|
||||
import { MessageNode } from "./components/message/MessageNode";
|
||||
import { DelayNode } from "./components/delay/DelayNode";
|
||||
|
||||
import "./style.css";
|
||||
import {
|
||||
ConditionType,
|
||||
MessageType,
|
||||
DelayType,
|
||||
} from "./nodeFunctions";
|
||||
import { Agent } from "@/types/agent";
|
||||
|
||||
type AgentNodeType = Node<
|
||||
{
|
||||
label?: string;
|
||||
agent?: Agent;
|
||||
},
|
||||
"agent-node"
|
||||
>;
|
||||
|
||||
type MessageNodeType = Node<
|
||||
{
|
||||
label?: string;
|
||||
message?: MessageType;
|
||||
},
|
||||
"message-node"
|
||||
>;
|
||||
|
||||
type DelayNodeType = Node<
|
||||
{
|
||||
label?: string;
|
||||
delay?: DelayType;
|
||||
},
|
||||
"delay-node"
|
||||
>;
|
||||
|
||||
type ConditionNodeType = Node<
|
||||
{
|
||||
label?: string;
|
||||
integration?: string;
|
||||
icon?: string;
|
||||
conditions?: ConditionType[];
|
||||
},
|
||||
"condition-node"
|
||||
>;
|
||||
|
||||
export type AppNode =
|
||||
| BuiltInNode
|
||||
| StartNodeType
|
||||
| AgentNodeType
|
||||
| ConditionNodeType
|
||||
| MessageNodeType
|
||||
| DelayNodeType;
|
||||
|
||||
export type NodeType = AppNode["type"];
|
||||
|
||||
export const initialNodes: AppNode[] = [
|
||||
{
|
||||
id: "start-node",
|
||||
type: "start-node",
|
||||
position: { x: -100, y: 100 },
|
||||
data: {
|
||||
label: "Start",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const nodeTypes = {
|
||||
"start-node": StartNode,
|
||||
"agent-node": AgentNode,
|
||||
"message-node": MessageNode,
|
||||
"condition-node": ConditionNode,
|
||||
"delay-node": DelayNode,
|
||||
} satisfies NodeTypes;
|
||||
65
frontend/app/agents/workflows/nodes/nodeFunctions.ts
Normal file
65
frontend/app/agents/workflows/nodes/nodeFunctions.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @author: Victor Calazans - Implementation of Delay types │
|
||||
│ @file: /app/agents/workflows/nodes/nodeFunctions.ts │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Delay node functionality developed by: Victor Calazans │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Delay implementation date: May 17, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable no-unused-vars */
|
||||
export enum ConditionTypeEnum {
|
||||
PREVIOUS_OUTPUT = "previous-output",
|
||||
}
|
||||
|
||||
export enum MessageTypeEnum {
|
||||
TEXT = "text",
|
||||
}
|
||||
|
||||
export enum DelayUnitEnum {
|
||||
SECONDS = "seconds",
|
||||
MINUTES = "minutes",
|
||||
HOURS = "hours",
|
||||
DAYS = "days",
|
||||
}
|
||||
|
||||
export type MessageType = {
|
||||
type: MessageTypeEnum;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type DelayType = {
|
||||
value: number;
|
||||
unit: DelayUnitEnum;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type ConditionType = {
|
||||
id: string;
|
||||
type: ConditionTypeEnum;
|
||||
data?: any;
|
||||
};
|
||||
|
||||
54
frontend/app/agents/workflows/nodes/style.css
Normal file
54
frontend/app/agents/workflows/nodes/style.css
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/nodes/style.css │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
/* .react-flow__handle {
|
||||
background-color: #8492A6;
|
||||
border-radius: 50%;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
} */
|
||||
|
||||
/* .react-flow__handle-right {
|
||||
right: -6px;
|
||||
top: 88%;
|
||||
transform: translateY(-75%);
|
||||
background-color: #f5f5f5 !important;
|
||||
border: 3px solid #8492A6 !important;
|
||||
} */
|
||||
|
||||
/* .react-flow__handle-left {
|
||||
left: 0px;
|
||||
top: 40%;
|
||||
width: 60px;
|
||||
border-radius: 0;
|
||||
height: 50%;
|
||||
transform: translateY(-75%);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
} */
|
||||
218
frontend/app/agents/workflows/page.tsx
Normal file
218
frontend/app/agents/workflows/page.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/page.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef, Suspense } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import Canva from "./Canva";
|
||||
import { Agent } from "@/types/agent";
|
||||
import { getAgent, updateAgent } from "@/services/agentService";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft, Save, Download, PlayIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ReactFlowProvider } from "@xyflow/react";
|
||||
import { DnDProvider } from "@/contexts/DnDContext";
|
||||
import { NodeDataProvider } from "@/contexts/NodeDataContext";
|
||||
import { SourceClickProvider } from "@/contexts/SourceClickContext";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { AgentTestChatModal } from "./nodes/components/agent/AgentTestChatModal";
|
||||
|
||||
function WorkflowsContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const agentId = searchParams.get("agentId");
|
||||
const [agent, setAgent] = useState<Agent | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const canvaRef = useRef<any>(null);
|
||||
const { toast } = useToast();
|
||||
const [isTestModalOpen, setIsTestModalOpen] = useState(false);
|
||||
|
||||
const user =
|
||||
typeof window !== "undefined"
|
||||
? JSON.parse(localStorage.getItem("user") || "{}")
|
||||
: {};
|
||||
const clientId = user?.client_id || "";
|
||||
|
||||
useEffect(() => {
|
||||
if (agentId && clientId) {
|
||||
setLoading(true);
|
||||
getAgent(agentId, clientId)
|
||||
.then((res) => {
|
||||
setAgent(res.data);
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(
|
||||
"current_workflow_agent",
|
||||
JSON.stringify(res.data)
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error loading agent:", err);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}, [agentId, clientId]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.removeItem("current_workflow_agent");
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSaveWorkflow = async () => {
|
||||
if (!agent || !canvaRef.current) return;
|
||||
|
||||
try {
|
||||
const { nodes, edges } = canvaRef.current.getFlowData();
|
||||
|
||||
const workflow = {
|
||||
nodes,
|
||||
edges,
|
||||
};
|
||||
|
||||
await updateAgent(agent.id, {
|
||||
...agent,
|
||||
config: {
|
||||
...agent.config,
|
||||
workflow,
|
||||
},
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "Workflow saved",
|
||||
description: "The changes were saved successfully",
|
||||
});
|
||||
|
||||
canvaRef.current.setHasChanges(false);
|
||||
} catch (error) {
|
||||
console.error("Error saving workflow:", error);
|
||||
toast({
|
||||
title: "Error saving workflow",
|
||||
description: "Unable to save the changes",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="w-full h-screen bg-[#121212] flex items-center justify-center">
|
||||
<div className="text-white text-xl">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-screen flex flex-col" data-workflow-page="true">
|
||||
{/* Header with controls */}
|
||||
<div className="w-full bg-[#121212] py-4 px-6 z-10 flex items-center justify-between border-b border-neutral-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="/agents">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-neutral-800 border-neutral-700 text-neutral-200 hover:bg-neutral-700"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Agents
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{agent && (
|
||||
<div className="bg-neutral-800 px-4 py-2 rounded-md">
|
||||
<h2 className="text-neutral-200 font-medium">
|
||||
{agent.name} -{" "}
|
||||
<span className="text-neutral-400 text-sm">{agent.type}</span>
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-neutral-800 border-neutral-700 text-neutral-200 hover:bg-neutral-700"
|
||||
onClick={handleSaveWorkflow}
|
||||
>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
Save
|
||||
</Button>
|
||||
{agent && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-green-800 border-green-700 text-green-200 hover:bg-green-700"
|
||||
onClick={() => setIsTestModalOpen(true)}
|
||||
>
|
||||
<PlayIcon className="h-4 w-4 mr-2" />
|
||||
Test Workflow
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content area */}
|
||||
<div className="flex-1 relative overflow-hidden">
|
||||
{agent && isTestModalOpen && (
|
||||
<AgentTestChatModal
|
||||
open={isTestModalOpen}
|
||||
onOpenChange={setIsTestModalOpen}
|
||||
agent={agent}
|
||||
canvasRef={canvaRef} // Pass the canvas reference to allow visualization of running nodes
|
||||
/>
|
||||
)}
|
||||
|
||||
<NodeDataProvider>
|
||||
<SourceClickProvider>
|
||||
<DnDProvider>
|
||||
<ReactFlowProvider>
|
||||
<Canva agent={agent} ref={canvaRef} data-canvas-ref="true" />
|
||||
</ReactFlowProvider>
|
||||
</DnDProvider>
|
||||
</SourceClickProvider>
|
||||
</NodeDataProvider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WorkflowsPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="w-full h-screen bg-[#121212] flex items-center justify-center">
|
||||
<div className="text-white text-xl">Loading...</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<WorkflowsContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
199
frontend/app/agents/workflows/utils.ts
Normal file
199
frontend/app/agents/workflows/utils.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/agents/workflows/utils.ts │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
import { Node, NodePositionChange, XYPosition } from "@xyflow/react";
|
||||
|
||||
type GetHelperLinesResult = {
|
||||
horizontal?: number;
|
||||
vertical?: number;
|
||||
snapPosition: Partial<XYPosition>;
|
||||
};
|
||||
|
||||
// this utility function can be called with a position change (inside onNodesChange)
|
||||
// it checks all other nodes and calculated the helper line positions and the position where the current node should snap to
|
||||
export function getHelperLines(
|
||||
change: NodePositionChange,
|
||||
nodes: Node[],
|
||||
distance = 5,
|
||||
): GetHelperLinesResult {
|
||||
const defaultResult = {
|
||||
horizontal: undefined,
|
||||
vertical: undefined,
|
||||
snapPosition: { x: undefined, y: undefined },
|
||||
};
|
||||
const nodeA = nodes.find((node) => node.id === change.id);
|
||||
|
||||
if (!nodeA || !change.position) {
|
||||
return defaultResult;
|
||||
}
|
||||
|
||||
const nodeABounds = {
|
||||
left: change.position.x,
|
||||
right: change.position.x + (nodeA.measured?.width ?? 0),
|
||||
top: change.position.y,
|
||||
bottom: change.position.y + (nodeA.measured?.height ?? 0),
|
||||
width: nodeA.measured?.width ?? 0,
|
||||
height: nodeA.measured?.height ?? 0,
|
||||
};
|
||||
|
||||
let horizontalDistance = distance;
|
||||
let verticalDistance = distance;
|
||||
|
||||
return nodes
|
||||
.filter((node) => node.id !== nodeA.id)
|
||||
.reduce<GetHelperLinesResult>((result, nodeB) => {
|
||||
const nodeBBounds = {
|
||||
left: nodeB.position.x,
|
||||
right: nodeB.position.x + (nodeB.measured?.width ?? 0),
|
||||
top: nodeB.position.y,
|
||||
bottom: nodeB.position.y + (nodeB.measured?.height ?? 0),
|
||||
width: nodeB.measured?.width ?? 0,
|
||||
height: nodeB.measured?.height ?? 0,
|
||||
};
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A |
|
||||
// |___________|
|
||||
// |
|
||||
// |
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | B |
|
||||
// |___________|
|
||||
const distanceLeftLeft = Math.abs(nodeABounds.left - nodeBBounds.left);
|
||||
|
||||
if (distanceLeftLeft < verticalDistance) {
|
||||
result.snapPosition.x = nodeBBounds.left;
|
||||
result.vertical = nodeBBounds.left;
|
||||
verticalDistance = distanceLeftLeft;
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A |
|
||||
// |___________|
|
||||
// |
|
||||
// |
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | B |
|
||||
// |___________|
|
||||
const distanceRightRight = Math.abs(
|
||||
nodeABounds.right - nodeBBounds.right,
|
||||
);
|
||||
|
||||
if (distanceRightRight < verticalDistance) {
|
||||
result.snapPosition.x = nodeBBounds.right - nodeABounds.width;
|
||||
result.vertical = nodeBBounds.right;
|
||||
verticalDistance = distanceRightRight;
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A |
|
||||
// |___________|
|
||||
// |
|
||||
// |
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | B |
|
||||
// |___________|
|
||||
const distanceLeftRight = Math.abs(nodeABounds.left - nodeBBounds.right);
|
||||
|
||||
if (distanceLeftRight < verticalDistance) {
|
||||
result.snapPosition.x = nodeBBounds.right;
|
||||
result.vertical = nodeBBounds.right;
|
||||
verticalDistance = distanceLeftRight;
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A |
|
||||
// |___________|
|
||||
// |
|
||||
// |
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | B |
|
||||
// |___________|
|
||||
const distanceRightLeft = Math.abs(nodeABounds.right - nodeBBounds.left);
|
||||
|
||||
if (distanceRightLeft < verticalDistance) {
|
||||
result.snapPosition.x = nodeBBounds.left - nodeABounds.width;
|
||||
result.vertical = nodeBBounds.left;
|
||||
verticalDistance = distanceRightLeft;
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|‾‾‾‾‾|‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A | | B |
|
||||
// |___________| |___________|
|
||||
const distanceTopTop = Math.abs(nodeABounds.top - nodeBBounds.top);
|
||||
|
||||
if (distanceTopTop < horizontalDistance) {
|
||||
result.snapPosition.y = nodeBBounds.top;
|
||||
result.horizontal = nodeBBounds.top;
|
||||
horizontalDistance = distanceTopTop;
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A |
|
||||
// |___________|_________________
|
||||
// | |
|
||||
// | B |
|
||||
// |___________|
|
||||
const distanceBottomTop = Math.abs(nodeABounds.bottom - nodeBBounds.top);
|
||||
|
||||
if (distanceBottomTop < horizontalDistance) {
|
||||
result.snapPosition.y = nodeBBounds.top - nodeABounds.height;
|
||||
result.horizontal = nodeBBounds.top;
|
||||
horizontalDistance = distanceBottomTop;
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾| |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A | | B |
|
||||
// |___________|_____|___________|
|
||||
const distanceBottomBottom = Math.abs(
|
||||
nodeABounds.bottom - nodeBBounds.bottom,
|
||||
);
|
||||
|
||||
if (distanceBottomBottom < horizontalDistance) {
|
||||
result.snapPosition.y = nodeBBounds.bottom - nodeABounds.height;
|
||||
result.horizontal = nodeBBounds.bottom;
|
||||
horizontalDistance = distanceBottomBottom;
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | B |
|
||||
// | |
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
||||
// | A |
|
||||
// |___________|
|
||||
const distanceTopBottom = Math.abs(nodeABounds.top - nodeBBounds.bottom);
|
||||
|
||||
if (distanceTopBottom < horizontalDistance) {
|
||||
result.snapPosition.y = nodeBBounds.bottom;
|
||||
result.horizontal = nodeBBounds.bottom;
|
||||
horizontalDistance = distanceTopBottom;
|
||||
}
|
||||
|
||||
return result;
|
||||
}, defaultResult);
|
||||
}
|
||||
497
frontend/app/chat/components/AgentInfoDialog.tsx
Normal file
497
frontend/app/chat/components/AgentInfoDialog.tsx
Normal file
@@ -0,0 +1,497 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/chat/components/AgentInfoDialog.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 14, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Agent } from "@/types/agent";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Bot,
|
||||
Code,
|
||||
WrenchIcon,
|
||||
Layers,
|
||||
Server,
|
||||
TagIcon,
|
||||
Share,
|
||||
Edit,
|
||||
Loader2,
|
||||
Download,
|
||||
} from "lucide-react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { listApiKeys, ApiKey } from "@/services/agentService";
|
||||
import { listMCPServers } from "@/services/mcpServerService";
|
||||
import { availableModels } from "@/types/aiModels";
|
||||
import { MCPServer } from "@/types/mcpServer";
|
||||
import { AgentForm } from "@/app/agents/forms/AgentForm";
|
||||
import { exportAsJson } from "@/lib/utils";
|
||||
|
||||
interface AgentInfoDialogProps {
|
||||
agent: Agent | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onAgentUpdated?: (updatedAgent: Agent) => void;
|
||||
}
|
||||
|
||||
export function AgentInfoDialog({
|
||||
agent,
|
||||
open,
|
||||
onOpenChange,
|
||||
onAgentUpdated,
|
||||
}: AgentInfoDialogProps) {
|
||||
const [activeTab, setActiveTab] = useState("info");
|
||||
const [isAgentFormOpen, setIsAgentFormOpen] = useState(false);
|
||||
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
|
||||
const [availableMCPs, setAvailableMCPs] = useState<MCPServer[]>([]);
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const user =
|
||||
typeof window !== "undefined"
|
||||
? JSON.parse(localStorage.getItem("user") || "{}")
|
||||
: {};
|
||||
const clientId = user?.client_id || "";
|
||||
|
||||
useEffect(() => {
|
||||
if (!clientId || !open) return;
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const [apiKeysResponse, mcpServersResponse] = await Promise.all([
|
||||
listApiKeys(clientId),
|
||||
listMCPServers(),
|
||||
]);
|
||||
|
||||
setApiKeys(apiKeysResponse.data);
|
||||
setAvailableMCPs(mcpServersResponse.data);
|
||||
} catch (error) {
|
||||
console.error("Error loading data for agent form:", error);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [clientId, open]);
|
||||
|
||||
const getAgentTypeName = (type: string) => {
|
||||
const agentTypes: Record<string, string> = {
|
||||
llm: "LLM Agent",
|
||||
a2a: "A2A Agent",
|
||||
sequential: "Sequential Agent",
|
||||
parallel: "Parallel Agent",
|
||||
loop: "Loop Agent",
|
||||
workflow: "Workflow Agent",
|
||||
task: "Task Agent",
|
||||
};
|
||||
return agentTypes[type] || type;
|
||||
};
|
||||
|
||||
const handleSaveAgent = async (agentData: Partial<Agent>) => {
|
||||
if (!agent?.id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const { updateAgent } = await import("@/services/agentService");
|
||||
const updated = await updateAgent(agent.id, agentData as any);
|
||||
|
||||
if (updated.data && onAgentUpdated) {
|
||||
onAgentUpdated(updated.data);
|
||||
}
|
||||
|
||||
setIsAgentFormOpen(false);
|
||||
} catch (error) {
|
||||
console.error("Error updating agent:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to export the agent as JSON
|
||||
const handleExportAgent = async () => {
|
||||
if (!agent) return;
|
||||
|
||||
try {
|
||||
// First fetch all agents to properly resolve agent_tools references
|
||||
const { listAgents } = await import("@/services/agentService");
|
||||
const allAgentsResponse = await listAgents(clientId, 0, 1000);
|
||||
const allAgents = allAgentsResponse.data || [];
|
||||
|
||||
exportAsJson(
|
||||
agent,
|
||||
`agent-${agent.name.replace(/\s+/g, "-").toLowerCase()}-${agent.id.substring(0, 8)}`,
|
||||
true,
|
||||
allAgents
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error exporting agent:", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!agent) return null;
|
||||
|
||||
const getToolsCount = () => {
|
||||
let count = 0;
|
||||
if (agent.config?.tools) count += agent.config.tools.length;
|
||||
if (agent.config?.custom_tools?.http_tools)
|
||||
count += agent.config.custom_tools.http_tools.length;
|
||||
if (agent.config?.agent_tools)
|
||||
count += agent.config.agent_tools.length;
|
||||
return count;
|
||||
};
|
||||
|
||||
const getSubAgentsCount = () => {
|
||||
return agent.config?.sub_agents?.length || 0;
|
||||
};
|
||||
|
||||
const getMCPServersCount = () => {
|
||||
let count = 0;
|
||||
if (agent.config?.mcp_servers) count += agent.config.mcp_servers.length;
|
||||
if (agent.config?.custom_mcp_servers)
|
||||
count += agent.config.custom_mcp_servers.length;
|
||||
return count;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[85vh] flex flex-col overflow-hidden bg-neutral-900 border-neutral-700">
|
||||
<DialogHeader className="flex flex-row items-start justify-between pb-2">
|
||||
<div>
|
||||
<DialogTitle className="text-xl text-white flex items-center gap-2">
|
||||
<Bot className="h-5 w-5 text-emerald-400" />
|
||||
{agent.name}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400 mt-1">
|
||||
{agent.description || "No description available"}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-neutral-800 border-neutral-700 text-emerald-400"
|
||||
>
|
||||
{getAgentTypeName(agent.type)}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-full bg-neutral-800 border-neutral-700 hover:bg-emerald-900 hover:text-emerald-400"
|
||||
onClick={handleExportAgent}
|
||||
title="Export agent as JSON"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-full bg-neutral-800 border-neutral-700 hover:bg-emerald-900 hover:text-emerald-400"
|
||||
onClick={() => setIsAgentFormOpen(true)}
|
||||
title="Edit agent"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="flex-1 overflow-hidden flex flex-col"
|
||||
>
|
||||
<TabsList className="bg-neutral-800 p-1 border-b border-neutral-700">
|
||||
<TabsTrigger
|
||||
value="info"
|
||||
className="data-[state=active]:bg-neutral-700 data-[state=active]:text-emerald-400"
|
||||
>
|
||||
Information
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="tools"
|
||||
className="data-[state=active]:bg-neutral-700 data-[state=active]:text-emerald-400"
|
||||
>
|
||||
Tools
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="config"
|
||||
className="data-[state=active]:bg-neutral-700 data-[state=active]:text-emerald-400"
|
||||
>
|
||||
Configuration
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<ScrollArea className="flex-1 p-4">
|
||||
<TabsContent value="info" className="mt-0 space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="bg-neutral-800 p-3 rounded-md border border-neutral-700 flex flex-col items-center justify-center text-center">
|
||||
<Code className="h-5 w-5 text-emerald-400 mb-1" />
|
||||
<span className="text-xs text-neutral-400">Model</span>
|
||||
<span className="text-sm text-neutral-200 mt-1 font-medium">
|
||||
{agent.model || "Not specified"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-neutral-800 p-3 rounded-md border border-neutral-700 flex flex-col items-center justify-center text-center">
|
||||
<TagIcon className="h-5 w-5 text-emerald-400 mb-1" />
|
||||
<span className="text-xs text-neutral-400">Tools</span>
|
||||
<span className="text-sm text-neutral-200 mt-1 font-medium">
|
||||
{getToolsCount()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-neutral-800 p-3 rounded-md border border-neutral-700 flex flex-col items-center justify-center text-center">
|
||||
<Layers className="h-5 w-5 text-emerald-400 mb-1" />
|
||||
<span className="text-xs text-neutral-400">
|
||||
Sub-agents
|
||||
</span>
|
||||
<span className="text-sm text-neutral-200 mt-1 font-medium">
|
||||
{getSubAgentsCount()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-neutral-800 p-4 rounded-md border border-neutral-700">
|
||||
<h3 className="text-sm font-medium text-emerald-400 mb-2">
|
||||
Agent Role
|
||||
</h3>
|
||||
<p className="text-neutral-300 text-sm">
|
||||
{agent.role || "Not specified"}
|
||||
</p>
|
||||
<h3 className="text-sm font-medium text-emerald-400 mb-2">
|
||||
Agent Goal
|
||||
</h3>
|
||||
<p className="text-neutral-300 text-sm">
|
||||
{agent.goal || "Not specified"}
|
||||
</p>
|
||||
{/* agent instructions: agent.instruction */}
|
||||
<h3 className="text-sm font-medium text-emerald-400 mb-2">
|
||||
Agent Instructions
|
||||
</h3>
|
||||
<div className="bg-neutral-900 p-3 rounded-md border border-neutral-700 max-h-[200px] overflow-y-auto">
|
||||
<pre className="text-xs text-neutral-300 whitespace-pre-wrap font-mono">
|
||||
{agent.instruction || "No instructions provided"}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="tools" className="mt-0 space-y-4">
|
||||
{getToolsCount() > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{/* Built-in tools */}
|
||||
{agent.config?.tools && agent.config.tools.length > 0 && (
|
||||
<div className="bg-neutral-800 p-4 rounded-md border border-neutral-700">
|
||||
<h3 className="text-sm font-medium text-emerald-400 mb-2">
|
||||
Built-in Tools
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{agent.config.tools.map((tool, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="outline"
|
||||
className="bg-neutral-900 border-neutral-700 text-neutral-300 p-2 justify-start"
|
||||
>
|
||||
<TagIcon className="h-3.5 w-3.5 mr-1.5 text-emerald-400" />
|
||||
{typeof tool === 'string' ? tool : 'Custom Tool'}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Agent Tools */}
|
||||
{agent.config?.agent_tools && agent.config.agent_tools.length > 0 && (
|
||||
<div className="bg-neutral-800 p-4 rounded-md border border-neutral-700">
|
||||
<h3 className="text-sm font-medium text-emerald-400 mb-2">
|
||||
Agent Tools
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{agent.config.agent_tools.map((agentId, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-neutral-900 p-2 rounded-md border border-neutral-700 flex items-center"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Bot className="h-3.5 w-3.5 mr-2 text-emerald-400" />
|
||||
<span className="text-sm text-neutral-300">
|
||||
{agents.find(a => a.id === agentId)?.name || agentId}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom HTTP tools */}
|
||||
{agent.config?.custom_tools?.http_tools &&
|
||||
agent.config.custom_tools.http_tools.length > 0 && (
|
||||
<div className="bg-neutral-800 p-4 rounded-md border border-neutral-700">
|
||||
<h3 className="text-sm font-medium text-emerald-400 mb-2">
|
||||
Custom HTTP Tools
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{agent.config.custom_tools.http_tools.map(
|
||||
(tool, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-neutral-900 p-2 rounded-md border border-neutral-700 flex items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Server className="h-3.5 w-3.5 mr-2 text-emerald-400" />
|
||||
<span className="text-sm text-neutral-300">
|
||||
{tool.name}
|
||||
</span>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs bg-neutral-800 border-neutral-700 text-emerald-400"
|
||||
>
|
||||
{tool.method}
|
||||
</Badge>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-neutral-800 p-6 rounded-md border border-neutral-700 text-center">
|
||||
<TagIcon className="h-8 w-8 text-neutral-600 mx-auto mb-2" />
|
||||
<p className="text-neutral-400">
|
||||
This agent has no tools configured
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="config" className="mt-0 space-y-4">
|
||||
<div className="space-y-3">
|
||||
{/* MCP Servers */}
|
||||
<div className="bg-neutral-800 p-4 rounded-md border border-neutral-700">
|
||||
<h3 className="text-sm font-medium text-emerald-400 mb-2">
|
||||
MCP Servers
|
||||
</h3>
|
||||
{getMCPServersCount() > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{agent.config?.mcp_servers &&
|
||||
agent.config.mcp_servers.map((mcp, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-neutral-900 p-2 rounded-md border border-neutral-700 flex items-center"
|
||||
>
|
||||
<Server className="h-3.5 w-3.5 mr-2 text-emerald-400" />
|
||||
<span className="text-sm text-neutral-300">
|
||||
{mcp.id}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{agent.config?.custom_mcp_servers &&
|
||||
agent.config.custom_mcp_servers.map((mcp, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-neutral-900 p-2 rounded-md border border-neutral-700 flex items-center"
|
||||
>
|
||||
<Server className="h-3.5 w-3.5 mr-2 text-yellow-400" />
|
||||
<span className="text-sm text-neutral-300">
|
||||
{mcp.url}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-neutral-400 text-sm">
|
||||
No MCP servers configured
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
<div className="bg-neutral-800 p-4 rounded-md border border-neutral-700">
|
||||
<h3 className="text-sm font-medium text-emerald-400 mb-2">
|
||||
API Key
|
||||
</h3>
|
||||
<p className="text-neutral-300 text-sm">
|
||||
{agent.api_key_id
|
||||
? `Key ID: ${agent.api_key_id}`
|
||||
: "No API key configured"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</ScrollArea>
|
||||
</Tabs>
|
||||
|
||||
<DialogFooter className="pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="bg-neutral-800 hover:bg-neutral-700 border-neutral-700 text-neutral-300"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Agent Edit Form Dialog */}
|
||||
{isAgentFormOpen && agent && (
|
||||
<AgentForm
|
||||
open={isAgentFormOpen}
|
||||
onOpenChange={setIsAgentFormOpen}
|
||||
initialValues={agent}
|
||||
apiKeys={apiKeys}
|
||||
availableModels={availableModels}
|
||||
availableMCPs={availableMCPs}
|
||||
agents={agents}
|
||||
onOpenApiKeysDialog={() => {}}
|
||||
onOpenMCPDialog={() => {}}
|
||||
onOpenCustomMCPDialog={() => {}}
|
||||
onSave={handleSaveAgent}
|
||||
isLoading={isLoading}
|
||||
getAgentNameById={(id) => id}
|
||||
clientId={clientId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
158
frontend/app/chat/components/AttachedFiles.tsx
Normal file
158
frontend/app/chat/components/AttachedFiles.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/chat/components/AttachedFiles.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: August 24, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { formatFileSize, isImageFile } from "@/lib/file-utils";
|
||||
import { File, FileText, Download, Image } from "lucide-react";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
|
||||
interface AttachedFile {
|
||||
filename: string;
|
||||
content_type: string;
|
||||
data?: string;
|
||||
size?: number;
|
||||
preview_url?: string;
|
||||
}
|
||||
|
||||
interface AttachedFilesProps {
|
||||
files: AttachedFile[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function AttachedFiles({ files, className = "" }: AttachedFilesProps) {
|
||||
if (!files || files.length === 0) return null;
|
||||
|
||||
const downloadFile = (file: AttachedFile) => {
|
||||
if (!file.data) {
|
||||
toast({
|
||||
title: "File without data for download",
|
||||
description: file.filename,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const link = document.createElement("a");
|
||||
const dataUrl = file.data.startsWith("data:")
|
||||
? file.data
|
||||
: `data:${file.content_type};base64,${file.data}`;
|
||||
|
||||
link.href = dataUrl;
|
||||
link.download = file.filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error downloading file",
|
||||
description: file.filename,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-2 mt-2 ${className}`}>
|
||||
<div className="text-xs text-neutral-400 mb-1">Attached files:</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{files
|
||||
.map((file, index) => {
|
||||
if (!file.data) {
|
||||
toast({
|
||||
title: "File without data for display",
|
||||
description: file.filename,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-col bg-[#333] rounded-md overflow-hidden border border-[#444] hover:border-[#666] transition-colors"
|
||||
>
|
||||
{isImageFile(file.content_type) && file.data && (
|
||||
<div className="w-full max-w-[200px] h-[120px] bg-black flex items-center justify-center">
|
||||
<img
|
||||
src={
|
||||
file.preview_url ||
|
||||
(file.data.startsWith("data:")
|
||||
? file.data
|
||||
: `data:${file.content_type};base64,${file.data}`)
|
||||
}
|
||||
alt={file.filename}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
onError={(e) => {
|
||||
toast({
|
||||
title: "Error loading image",
|
||||
description: file.filename,
|
||||
});
|
||||
(e.target as HTMLImageElement).src =
|
||||
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZjY2NjYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS1pbWFnZS1vZmYiPjxsaW5lIHgxPSIyIiB5MT0iMiIgeDI9IjIyIiB5Mj0iMjIiLz48PHJlY3QgdyA";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-2 flex items-center gap-2">
|
||||
<div className="flex-shrink-0">
|
||||
{isImageFile(file.content_type) ? (
|
||||
<Image className="h-4 w-4 text-emerald-400" />
|
||||
) : file.content_type === "application/pdf" ? (
|
||||
<FileText className="h-4 w-4 text-emerald-400" />
|
||||
) : (
|
||||
<File className="h-4 w-4 text-emerald-400" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs font-medium truncate max-w-[150px]">
|
||||
{file.filename}
|
||||
</div>
|
||||
{file.size && (
|
||||
<div className="text-[10px] text-neutral-400">
|
||||
{formatFileSize(file.size)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{file.data && (
|
||||
<button
|
||||
onClick={() => downloadFile(file)}
|
||||
className="text-emerald-400 hover:text-white transition-colors"
|
||||
title="Download"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
.filter(Boolean)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
190
frontend/app/chat/components/ChatContainer.tsx
Normal file
190
frontend/app/chat/components/ChatContainer.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/chat/components/ChatContainer.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 14, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { MessageSquare, Loader2, Bot, Zap } from "lucide-react";
|
||||
import { ChatMessage } from "./ChatMessage";
|
||||
import { ChatInput } from "./ChatInput";
|
||||
import { ChatMessage as ChatMessageType } from "@/services/sessionService";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface FunctionMessageContent {
|
||||
title: string;
|
||||
content: string;
|
||||
author?: string;
|
||||
}
|
||||
|
||||
interface ChatContainerProps {
|
||||
messages: ChatMessageType[];
|
||||
isLoading: boolean;
|
||||
onSendMessage: (message: string) => void;
|
||||
agentColor: string;
|
||||
expandedFunctions: Record<string, boolean>;
|
||||
toggleFunctionExpansion: (messageId: string) => void;
|
||||
containsMarkdown: (text: string) => boolean;
|
||||
getMessageText: (message: ChatMessageType) => string | FunctionMessageContent;
|
||||
agentName?: string;
|
||||
containerClassName?: string;
|
||||
messagesContainerClassName?: string;
|
||||
inputContainerClassName?: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
export function ChatContainer({
|
||||
messages,
|
||||
isLoading,
|
||||
onSendMessage,
|
||||
agentColor,
|
||||
expandedFunctions,
|
||||
toggleFunctionExpansion,
|
||||
containsMarkdown,
|
||||
getMessageText,
|
||||
agentName = "Agent",
|
||||
containerClassName = "",
|
||||
messagesContainerClassName = "",
|
||||
inputContainerClassName = "",
|
||||
sessionId,
|
||||
}: ChatContainerProps) {
|
||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [isInitializing, setIsInitializing] = useState(false);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (messagesContainerRef.current) {
|
||||
messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (messages.length > 0) {
|
||||
setTimeout(scrollToBottom, 100);
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
// Simulate initial loading for smoother UX
|
||||
useEffect(() => {
|
||||
if (sessionId) {
|
||||
setIsInitializing(true);
|
||||
const timer = setTimeout(() => {
|
||||
setIsInitializing(false);
|
||||
}, 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [sessionId]);
|
||||
|
||||
const isEmpty = messages.length === 0;
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
"flex-1 flex flex-col overflow-hidden bg-gradient-to-b from-neutral-900 to-neutral-950",
|
||||
containerClassName
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex-1 overflow-hidden p-5",
|
||||
messagesContainerClassName
|
||||
)}
|
||||
style={{ filter: isLoading && !isInitializing ? "blur(1px)" : "none" }}
|
||||
>
|
||||
<ScrollArea
|
||||
ref={messagesContainerRef}
|
||||
className="h-full pr-4"
|
||||
>
|
||||
{isInitializing ? (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-emerald-500 to-emerald-700 flex items-center justify-center shadow-lg mb-4 animate-pulse">
|
||||
<Zap className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<p className="text-neutral-400 mb-2">Loading conversation...</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-bounce"
|
||||
style={{ animationDelay: '0ms' }}></span>
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-bounce"
|
||||
style={{ animationDelay: '150ms' }}></span>
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-bounce"
|
||||
style={{ animationDelay: '300ms' }}></span>
|
||||
</div>
|
||||
</div>
|
||||
) : isEmpty ? (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center p-6">
|
||||
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-blue-500/20 to-emerald-500/20 flex items-center justify-center shadow-lg mb-5 border border-emerald-500/30">
|
||||
<MessageSquare className="h-6 w-6 text-emerald-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-neutral-300 mb-2">
|
||||
{`Chat with ${agentName}`}
|
||||
</h3>
|
||||
<p className="text-neutral-500 text-sm max-w-md">
|
||||
Type your message below to start the conversation. This chat will help you interact with the agent and explore its capabilities.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6 py-4 flex-1">
|
||||
{messages.map((message) => {
|
||||
const messageContent = getMessageText(message);
|
||||
const isExpanded = expandedFunctions[message.id] || false;
|
||||
|
||||
return (
|
||||
<ChatMessage
|
||||
key={message.id}
|
||||
message={message}
|
||||
agentColor={agentColor}
|
||||
isExpanded={isExpanded}
|
||||
toggleExpansion={toggleFunctionExpansion}
|
||||
containsMarkdown={containsMarkdown}
|
||||
messageContent={messageContent}
|
||||
sessionId={sessionId}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<div className={cn(
|
||||
"p-3 border-t border-neutral-800 bg-neutral-900",
|
||||
inputContainerClassName
|
||||
)}>
|
||||
{isLoading && !isInitializing && (
|
||||
<div className="px-4 py-2 mb-3 rounded-lg bg-neutral-800/50 text-sm text-neutral-400 flex items-center">
|
||||
<Loader2 className="h-3 w-3 mr-2 animate-spin text-emerald-400" />
|
||||
Agent is thinking...
|
||||
</div>
|
||||
)}
|
||||
<ChatInput
|
||||
onSendMessage={onSendMessage}
|
||||
isLoading={isLoading}
|
||||
placeholder="Type your message..."
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
270
frontend/app/chat/components/ChatInput.tsx
Normal file
270
frontend/app/chat/components/ChatInput.tsx
Normal file
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/chat/components/ChatInput.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 14, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import React, { useState, useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Loader2, Send, Paperclip, X, Image, FileText, File } from "lucide-react";
|
||||
import { FileData, formatFileSize, isImageFile } from "@/lib/file-utils";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
|
||||
interface ChatInputProps {
|
||||
onSendMessage: (message: string, files?: FileData[]) => void;
|
||||
isLoading?: boolean;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
containerClassName?: string;
|
||||
autoFocus?: boolean;
|
||||
}
|
||||
|
||||
export function ChatInput({
|
||||
onSendMessage,
|
||||
isLoading = false,
|
||||
placeholder = "Type your message...",
|
||||
className = "",
|
||||
buttonClassName = "",
|
||||
containerClassName = "",
|
||||
autoFocus = true,
|
||||
}: ChatInputProps) {
|
||||
const [messageInput, setMessageInput] = useState("");
|
||||
const [selectedFiles, setSelectedFiles] = useState<FileData[]>([]);
|
||||
const [resetFileUpload, setResetFileUpload] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// Autofocus the textarea when the component is mounted
|
||||
React.useEffect(() => {
|
||||
// Small timeout to ensure focus is applied after the complete rendering
|
||||
if (autoFocus) {
|
||||
const timer = setTimeout(() => {
|
||||
if (textareaRef.current && !isLoading) {
|
||||
textareaRef.current.focus();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isLoading, autoFocus]);
|
||||
|
||||
const handleSendMessage = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!messageInput.trim() && selectedFiles.length === 0) return;
|
||||
|
||||
onSendMessage(messageInput, selectedFiles.length > 0 ? selectedFiles : undefined);
|
||||
|
||||
setMessageInput("");
|
||||
setSelectedFiles([]);
|
||||
|
||||
setResetFileUpload(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setResetFileUpload(false);
|
||||
// Keep the focus on the textarea after sending the message
|
||||
if (autoFocus && textareaRef.current) {
|
||||
textareaRef.current.focus();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
const textarea = document.querySelector("textarea");
|
||||
if (textarea) textarea.style.height = "auto";
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage(e as unknown as React.FormEvent);
|
||||
}
|
||||
};
|
||||
|
||||
const autoResizeTextarea = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const textarea = e.target;
|
||||
textarea.style.height = "auto";
|
||||
const maxHeight = 10 * 24;
|
||||
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
|
||||
textarea.style.height = `${newHeight}px`;
|
||||
setMessageInput(textarea.value);
|
||||
};
|
||||
|
||||
const handleFilesSelected = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.files || e.target.files.length === 0) return;
|
||||
|
||||
const newFiles = Array.from(e.target.files);
|
||||
const maxFileSize = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
if (selectedFiles.length + newFiles.length > 5) {
|
||||
toast({
|
||||
title: `You can only attach up to 5 files.`,
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const validFiles: FileData[] = [];
|
||||
|
||||
for (const file of newFiles) {
|
||||
if (file.size > maxFileSize) {
|
||||
toast({
|
||||
title: `File ${file.name} exceeds the maximum size of ${formatFileSize(maxFileSize)}.`,
|
||||
variant: "destructive",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
|
||||
const readFile = new Promise<string>((resolve, reject) => {
|
||||
reader.onload = () => {
|
||||
const base64 = reader.result as string;
|
||||
const base64Data = base64.split(',')[1];
|
||||
resolve(base64Data);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
});
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
const base64Data = await readFile;
|
||||
const previewUrl = URL.createObjectURL(file);
|
||||
|
||||
validFiles.push({
|
||||
filename: file.name,
|
||||
content_type: file.type,
|
||||
data: base64Data,
|
||||
size: file.size,
|
||||
preview_url: previewUrl
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error processing file:", error);
|
||||
toast({
|
||||
title: `Error processing file ${file.name}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
const updatedFiles = [...selectedFiles, ...validFiles];
|
||||
setSelectedFiles(updatedFiles);
|
||||
}
|
||||
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const openFileSelector = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col w-full ${containerClassName}`}>
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 px-2 mb-3 mt-1">
|
||||
{selectedFiles.map((file, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-1.5 bg-gradient-to-br from-neutral-800 to-neutral-900 text-white rounded-lg p-2 text-xs border border-neutral-700/50 shadow-sm"
|
||||
>
|
||||
{isImageFile(file.content_type) ? (
|
||||
<Image className="h-4 w-4 text-emerald-400" />
|
||||
) : file.content_type === 'application/pdf' ? (
|
||||
<FileText className="h-4 w-4 text-emerald-400" />
|
||||
) : (
|
||||
<File className="h-4 w-4 text-emerald-400" />
|
||||
)}
|
||||
<span className="max-w-[120px] truncate">{file.filename}</span>
|
||||
<span className="text-neutral-400">({formatFileSize(file.size)})</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
const updatedFiles = selectedFiles.filter((_, i) => i !== index);
|
||||
setSelectedFiles(updatedFiles);
|
||||
}}
|
||||
className="ml-1 text-neutral-400 hover:text-white transition-colors bg-neutral-700/30 rounded-full p-1"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form
|
||||
onSubmit={handleSendMessage}
|
||||
className="flex w-full items-center gap-2 px-2"
|
||||
>
|
||||
{selectedFiles.length < 5 && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
openFileSelector();
|
||||
}}
|
||||
type="button"
|
||||
className="flex items-center justify-center w-9 h-9 rounded-full hover:bg-neutral-800/60 text-neutral-400 hover:text-emerald-400 transition-all border border-neutral-700/30"
|
||||
title="Attach file"
|
||||
>
|
||||
<Paperclip className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<Textarea
|
||||
value={messageInput}
|
||||
onChange={autoResizeTextarea}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
className={`flex-1 bg-neutral-800/40 border-neutral-700/50 text-white focus-visible:ring-emerald-500/50 focus-visible:border-emerald-500/50 min-h-[40px] max-h-[240px] resize-none rounded-xl ${className}`}
|
||||
disabled={isLoading}
|
||||
rows={1}
|
||||
ref={textareaRef}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading || (!messageInput.trim() && selectedFiles.length === 0)}
|
||||
className={`bg-gradient-to-r from-emerald-500 to-emerald-600 text-white hover:from-emerald-600 hover:to-emerald-700 rounded-full shadow-md h-9 w-9 p-0 ${buttonClassName}`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Send className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFilesSelected}
|
||||
className="hidden"
|
||||
multiple
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
375
frontend/app/chat/components/ChatMessage.tsx
Normal file
375
frontend/app/chat/components/ChatMessage.tsx
Normal file
@@ -0,0 +1,375 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/chat/components/ChatMessage.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { ChatMessage as ChatMessageType } from "@/services/sessionService";
|
||||
import { ChevronDown, ChevronRight, Copy, Check, User, Bot, Terminal } from "lucide-react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { useState } from "react";
|
||||
import { InlineDataAttachments } from "./InlineDataAttachments";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface FunctionMessageContent {
|
||||
title: string;
|
||||
content: string;
|
||||
author?: string;
|
||||
}
|
||||
|
||||
interface AttachedFile {
|
||||
filename: string;
|
||||
content_type: string;
|
||||
data: string;
|
||||
size: number;
|
||||
preview_url?: string;
|
||||
}
|
||||
|
||||
interface ChatMessageProps {
|
||||
message: ChatMessageType;
|
||||
agentColor: string;
|
||||
isExpanded: boolean;
|
||||
toggleExpansion: (messageId: string) => void;
|
||||
containsMarkdown: (text: string) => boolean;
|
||||
messageContent: string | FunctionMessageContent;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
export function ChatMessage({
|
||||
message,
|
||||
agentColor,
|
||||
isExpanded,
|
||||
toggleExpansion,
|
||||
containsMarkdown,
|
||||
messageContent,
|
||||
sessionId,
|
||||
}: ChatMessageProps) {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const isUser = message.author === "user";
|
||||
const hasFunctionCall = message.content.parts.some(
|
||||
(part) => part.functionCall || part.function_call
|
||||
);
|
||||
const hasFunctionResponse = message.content.parts.some(
|
||||
(part) => part.functionResponse || part.function_response
|
||||
);
|
||||
const isFunctionMessage = hasFunctionCall || hasFunctionResponse;
|
||||
const isTaskExecutor = typeof messageContent === "object" &&
|
||||
"author" in messageContent &&
|
||||
typeof messageContent.author === "string" &&
|
||||
messageContent.author.endsWith("- Task executor");
|
||||
|
||||
const inlineDataParts = message.content.parts.filter(part => part.inline_data);
|
||||
const hasInlineData = inlineDataParts.length > 0;
|
||||
|
||||
const copyToClipboard = () => {
|
||||
const textToCopy = typeof messageContent === "string"
|
||||
? messageContent
|
||||
: messageContent.content;
|
||||
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
});
|
||||
};
|
||||
|
||||
// Generate appropriate avatar content
|
||||
const getAvatar = () => {
|
||||
if (isUser) {
|
||||
return (
|
||||
<Avatar className="bg-gradient-to-br from-emerald-500 to-emerald-700 shadow-md border-0">
|
||||
<AvatarFallback className="bg-transparent">
|
||||
<User className="h-4 w-4 text-white" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Avatar className={`shadow-md border-0 ${
|
||||
isFunctionMessage
|
||||
? "bg-gradient-to-br from-emerald-600 to-emerald-800"
|
||||
: "bg-gradient-to-br from-purple-600 to-purple-800"
|
||||
}`}>
|
||||
<AvatarFallback className="bg-transparent">
|
||||
{isFunctionMessage ?
|
||||
<Terminal className="h-4 w-4 text-white" /> :
|
||||
<Bot className="h-4 w-4 text-white" />
|
||||
}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
className="flex w-full"
|
||||
style={{
|
||||
justifyContent: isUser ? "flex-end" : "flex-start"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex gap-3 max-w-[90%]"
|
||||
style={{
|
||||
flexDirection: isUser ? "row-reverse" : "row"
|
||||
}}
|
||||
>
|
||||
{getAvatar()}
|
||||
<div
|
||||
className={`rounded-lg p-3 overflow-hidden relative group shadow-md ${
|
||||
isFunctionMessage || isTaskExecutor
|
||||
? "bg-gradient-to-br from-neutral-800 to-neutral-900 border border-neutral-700/50 text-emerald-300 font-mono text-sm"
|
||||
: isUser
|
||||
? "bg-emerald-500 text-white"
|
||||
: "bg-gradient-to-br from-neutral-800 to-neutral-900 border border-neutral-700/50 text-white"
|
||||
}`}
|
||||
style={{
|
||||
wordBreak: "break-word",
|
||||
maxWidth: "calc(100% - 3rem)",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
{isFunctionMessage || isTaskExecutor ? (
|
||||
<div className="w-full">
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer hover:bg-[#444] rounded px-1 py-0.5 transition-colors"
|
||||
onClick={() => toggleExpansion(message.id)}
|
||||
>
|
||||
{typeof messageContent === "object" &&
|
||||
"title" in messageContent && (
|
||||
<>
|
||||
<div className="flex-1 font-semibold">
|
||||
{(messageContent as FunctionMessageContent).title}
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-5 h-5 text-emerald-400">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isTaskExecutor && (
|
||||
<>
|
||||
<div className="flex-1 font-semibold">
|
||||
Task Execution
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-5 h-5 text-emerald-400">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="mt-2 pt-2 border-t border-[#555]">
|
||||
{typeof messageContent === "object" &&
|
||||
"content" in messageContent && (
|
||||
<div className="max-w-full overflow-x-auto">
|
||||
<pre className="whitespace-pre-wrap text-xs max-w-full" style={{
|
||||
wordWrap: "break-word",
|
||||
maxWidth: "100%",
|
||||
wordBreak: "break-all"
|
||||
}}>
|
||||
{(messageContent as FunctionMessageContent).content}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="markdown-content break-words max-w-full overflow-x-auto">
|
||||
{typeof messageContent === "object" &&
|
||||
"author" in messageContent &&
|
||||
messageContent.author !== "user" &&
|
||||
!isTaskExecutor && (
|
||||
<div className="text-xs text-neutral-400 mb-1">
|
||||
{messageContent.author}
|
||||
</div>
|
||||
)}
|
||||
{((typeof messageContent === "string" &&
|
||||
containsMarkdown(messageContent)) ||
|
||||
(typeof messageContent === "object" &&
|
||||
"content" in messageContent &&
|
||||
typeof messageContent.content === "string" &&
|
||||
containsMarkdown(messageContent.content))) &&
|
||||
!isTaskExecutor ? (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
h1: ({ ...props }) => (
|
||||
<h1 className="text-xl font-bold my-4" {...props} />
|
||||
),
|
||||
h2: ({ ...props }) => (
|
||||
<h2 className="text-lg font-bold my-3" {...props} />
|
||||
),
|
||||
h3: ({ ...props }) => (
|
||||
<h3 className="text-base font-bold my-2" {...props} />
|
||||
),
|
||||
h4: ({ ...props }) => (
|
||||
<h4 className="font-semibold my-2" {...props} />
|
||||
),
|
||||
p: ({ ...props }) => <p className="mb-3" {...props} />,
|
||||
ul: ({ ...props }) => (
|
||||
<ul
|
||||
className="list-disc pl-6 mb-3 space-y-1"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ol: ({ ...props }) => (
|
||||
<ol
|
||||
className="list-decimal pl-6 mb-3 space-y-1"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
li: ({ ...props }) => <li className="mb-1" {...props} />,
|
||||
a: ({ ...props }) => (
|
||||
<a
|
||||
className="text-emerald-300 underline hover:opacity-80 transition-opacity"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
blockquote: ({ ...props }) => (
|
||||
<blockquote
|
||||
className="border-l-4 border-[#444] pl-4 py-1 italic my-3 text-neutral-300"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
code: ({ className, children, ...props }: any) => {
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
const isInline =
|
||||
!match &&
|
||||
typeof children === "string" &&
|
||||
!children.includes("\n");
|
||||
|
||||
if (isInline) {
|
||||
return (
|
||||
<code
|
||||
className="bg-[#333] px-1.5 py-0.5 rounded text-emerald-300 text-sm font-mono"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-3 relative group/code">
|
||||
<div className="bg-[#1a1a1a] rounded-t-md border-b border-[#333] p-2 text-xs text-neutral-400 flex justify-between items-center">
|
||||
<span>{match?.[1] || "Code"}</span>
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="text-neutral-400 hover:text-emerald-300 transition-colors"
|
||||
title="Copy code"
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<pre className="bg-[#1a1a1a] p-3 rounded-b-md overflow-x-auto whitespace-pre text-sm">
|
||||
<code {...props}>{children}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
table: ({ ...props }) => (
|
||||
<div className="overflow-x-auto my-3">
|
||||
<table
|
||||
className="min-w-full border border-[#333] rounded"
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
thead: ({ ...props }) => (
|
||||
<thead className="bg-[#1a1a1a]" {...props} />
|
||||
),
|
||||
tbody: ({ ...props }) => <tbody {...props} />,
|
||||
tr: ({ ...props }) => (
|
||||
<tr
|
||||
className="border-b border-[#333] last:border-0"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
th: ({ ...props }) => (
|
||||
<th
|
||||
className="px-4 py-2 text-left text-xs font-semibold text-neutral-300"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
td: ({ ...props }) => (
|
||||
<td className="px-4 py-2 text-sm" {...props} />
|
||||
),
|
||||
}}
|
||||
>
|
||||
{typeof messageContent === "string"
|
||||
? messageContent
|
||||
: messageContent.content}
|
||||
</ReactMarkdown>
|
||||
) : (
|
||||
<div className="whitespace-pre-wrap">
|
||||
{typeof messageContent === "string"
|
||||
? messageContent
|
||||
: messageContent.content}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasInlineData && (
|
||||
<InlineDataAttachments parts={inlineDataParts} sessionId={sessionId} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="absolute top-2 right-2 p-1.5 rounded-full bg-neutral-800/80 text-neutral-400 hover:text-white opacity-0 group-hover:opacity-100 transition-all hover:bg-neutral-700/80"
|
||||
title="Copy message"
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
185
frontend/app/chat/components/FileUpload.tsx
Normal file
185
frontend/app/chat/components/FileUpload.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/chat/components/FileUpload.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: August 24, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { FileData, formatFileSize, isImageFile } from "@/lib/file-utils";
|
||||
import { Paperclip, X, Image, File, FileText } from "lucide-react";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
|
||||
interface FileUploadProps {
|
||||
onFilesSelected: (files: FileData[]) => void;
|
||||
maxFileSize?: number;
|
||||
maxFiles?: number;
|
||||
className?: string;
|
||||
reset?: boolean;
|
||||
}
|
||||
|
||||
export function FileUpload({
|
||||
onFilesSelected,
|
||||
maxFileSize = 10 * 1024 * 1024, // 10MB
|
||||
maxFiles = 5,
|
||||
className = "",
|
||||
reset = false, // Default false
|
||||
}: FileUploadProps) {
|
||||
const [selectedFiles, setSelectedFiles] = useState<FileData[]>([]);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (reset && selectedFiles.length > 0) {
|
||||
setSelectedFiles([]);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
}
|
||||
}, [reset]);
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.files || e.target.files.length === 0) return;
|
||||
|
||||
const newFiles = Array.from(e.target.files);
|
||||
|
||||
if (selectedFiles.length + newFiles.length > maxFiles) {
|
||||
toast({
|
||||
title: `You can only attach up to ${maxFiles} files.`,
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const validFiles: FileData[] = [];
|
||||
|
||||
for (const file of newFiles) {
|
||||
if (file.size > maxFileSize) {
|
||||
toast({
|
||||
title: `File ${file.name} exceeds the maximum size of ${formatFileSize(maxFileSize)}.`,
|
||||
variant: "destructive",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
|
||||
const readFile = new Promise<string>((resolve, reject) => {
|
||||
reader.onload = () => {
|
||||
const base64 = reader.result as string;
|
||||
const base64Data = base64.split(',')[1];
|
||||
resolve(base64Data);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
});
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
const base64Data = await readFile;
|
||||
const previewUrl = URL.createObjectURL(file);
|
||||
|
||||
validFiles.push({
|
||||
filename: file.name,
|
||||
content_type: file.type,
|
||||
data: base64Data,
|
||||
size: file.size,
|
||||
preview_url: previewUrl
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error processing file:", error);
|
||||
toast({
|
||||
title: `Error processing file ${file.name}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
const updatedFiles = [...selectedFiles, ...validFiles];
|
||||
setSelectedFiles(updatedFiles);
|
||||
onFilesSelected(updatedFiles);
|
||||
}
|
||||
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
const updatedFiles = selectedFiles.filter((_, i) => i !== index);
|
||||
setSelectedFiles(updatedFiles);
|
||||
onFilesSelected(updatedFiles);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex gap-2 items-center ${className}`}>
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className="flex gap-2 flex-wrap items-center flex-1">
|
||||
{selectedFiles.map((file, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-1 bg-[#333] text-white rounded-md p-1.5 text-xs group relative"
|
||||
>
|
||||
{isImageFile(file.content_type) ? (
|
||||
<Image className="h-4 w-4 text-emerald-400" />
|
||||
) : file.content_type === 'application/pdf' ? (
|
||||
<FileText className="h-4 w-4 text-emerald-400" />
|
||||
) : (
|
||||
<File className="h-4 w-4 text-emerald-400" />
|
||||
)}
|
||||
<span className="max-w-[120px] truncate">{file.filename}</span>
|
||||
<span className="text-neutral-400">({formatFileSize(file.size)})</span>
|
||||
<button
|
||||
onClick={() => removeFile(index)}
|
||||
className="ml-1 text-neutral-400 hover:text-white transition-colors"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedFiles.length < maxFiles && (
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
type="button"
|
||||
className="flex items-center justify-center w-8 h-8 rounded-full hover:bg-[#333] text-neutral-400 hover:text-emerald-400 transition-colors"
|
||||
title="Attach file"
|
||||
>
|
||||
<Paperclip className="h-5 w-5" />
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
multiple
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
182
frontend/app/chat/components/InlineDataAttachments.tsx
Normal file
182
frontend/app/chat/components/InlineDataAttachments.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/chat/components/InlineDataAttachments.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: August 29, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { formatFileSize, isImageFile } from "@/lib/file-utils";
|
||||
import { File, FileText, Download, Image } from "lucide-react";
|
||||
import { ChatPart } from "@/services/sessionService";
|
||||
|
||||
interface InlineDataAttachmentsProps {
|
||||
parts: ChatPart[];
|
||||
className?: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
interface ProcessedFile {
|
||||
filename: string;
|
||||
content_type: string;
|
||||
data: string;
|
||||
size: number;
|
||||
preview_url?: string;
|
||||
}
|
||||
|
||||
export function InlineDataAttachments({ parts, className = "", sessionId }: InlineDataAttachmentsProps) {
|
||||
const [processedFiles, setProcessedFiles] = useState<ProcessedFile[]>([]);
|
||||
const [isProcessed, setIsProcessed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isProcessed) return;
|
||||
|
||||
const validParts = parts.filter(part => part.inline_data && part.inline_data.data);
|
||||
|
||||
if (validParts.length === 0) {
|
||||
setIsProcessed(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const files = validParts.map((part, index) => {
|
||||
const { mime_type, data } = part.inline_data!;
|
||||
const extension = mime_type.split('/')[1] || 'file';
|
||||
|
||||
let filename = '';
|
||||
|
||||
if (part.inline_data?.metadata?.filename) {
|
||||
filename = part.inline_data.metadata.filename;
|
||||
}
|
||||
else if (part.file_data?.filename) {
|
||||
filename = part.file_data.filename;
|
||||
}
|
||||
else {
|
||||
filename = `media_${index + 1}.${extension}`;
|
||||
}
|
||||
|
||||
let preview_url = undefined;
|
||||
if (data && isImageFile(mime_type)) {
|
||||
preview_url = data.startsWith('data:')
|
||||
? data
|
||||
: `data:${mime_type};base64,${data}`;
|
||||
}
|
||||
|
||||
const fileData: ProcessedFile = {
|
||||
filename,
|
||||
content_type: mime_type,
|
||||
size: data.length,
|
||||
data,
|
||||
preview_url
|
||||
};
|
||||
|
||||
return fileData;
|
||||
});
|
||||
|
||||
setProcessedFiles(files);
|
||||
setIsProcessed(true);
|
||||
}, [parts, isProcessed]);
|
||||
|
||||
if (processedFiles.length === 0) return null;
|
||||
|
||||
const downloadFile = (file: ProcessedFile) => {
|
||||
try {
|
||||
const link = document.createElement("a");
|
||||
const dataUrl = file.data.startsWith('data:')
|
||||
? file.data
|
||||
: `data:${file.content_type};base64,${file.data}`;
|
||||
|
||||
link.href = dataUrl;
|
||||
link.download = file.filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} catch (error) {
|
||||
console.error(`Error downloading file ${file.filename}:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
const getFileUrl = (file: ProcessedFile) => {
|
||||
return file.preview_url || (file.data.startsWith('data:')
|
||||
? file.data
|
||||
: `data:${file.content_type};base64,${file.data}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-2 mt-2 ${className}`}>
|
||||
<div className="text-xs text-neutral-400 mb-1">
|
||||
<span>Attached files:</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{processedFiles.map((file, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-col bg-[#333] rounded-md overflow-hidden border border-[#444] hover:border-[#666] transition-colors"
|
||||
>
|
||||
{isImageFile(file.content_type) && (
|
||||
<div className="w-full max-w-[200px] h-[120px] bg-black flex items-center justify-center">
|
||||
<img
|
||||
src={getFileUrl(file)}
|
||||
alt={file.filename}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
onError={(e) => {
|
||||
console.error(`Error loading image ${file.filename}`);
|
||||
(e.target as HTMLImageElement).src = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZjY2NjYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS1pbWFnZS1vZmYiPjxsaW5lIHgxPSIyIiB5MT0iMiIgeDI9IjIyIiB5Mj0iMjIiLz48cmVjdCB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHg9IjIiIHk9IjIiIHJ4PSIyIiByeT0iMiIvPjxsaW5lIHgxPSI4IiB5MT0iMTAiIHgyPSI4IiB5Mj0iMTAiLz48bGluZSB4MT0iMTIiIHkxPSIxNCIgeDI9IjEyIiB5Mj0iMTQiLz48L3N2Zz4=";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-2 flex items-center gap-2">
|
||||
<div className="flex-shrink-0">
|
||||
{isImageFile(file.content_type) ? (
|
||||
<Image className="h-4 w-4 text-emerald-400" />
|
||||
) : file.content_type === "application/pdf" ? (
|
||||
<FileText className="h-4 w-4 text-emerald-400" />
|
||||
) : (
|
||||
<File className="h-4 w-4 text-emerald-400" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs font-medium truncate max-w-[150px]">
|
||||
{file.filename}
|
||||
</div>
|
||||
<div className="text-[10px] text-neutral-400">
|
||||
{formatFileSize(file.size)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => downloadFile(file)}
|
||||
className="text-emerald-400 hover:text-white transition-colors"
|
||||
title="Download"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
248
frontend/app/chat/components/SessionList.tsx
Normal file
248
frontend/app/chat/components/SessionList.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/chat/components/SessionList.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Search, Filter, Plus, Loader2 } from "lucide-react";
|
||||
import { ChatSession } from "@/services/sessionService";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
interface SessionListProps {
|
||||
sessions: ChatSession[];
|
||||
agents: any[];
|
||||
selectedSession: string | null;
|
||||
isLoading: boolean;
|
||||
searchTerm: string;
|
||||
selectedAgentFilter: string;
|
||||
showAgentFilter: boolean;
|
||||
setSearchTerm: (value: string) => void;
|
||||
setSelectedAgentFilter: (value: string) => void;
|
||||
setShowAgentFilter: (value: boolean) => void;
|
||||
setSelectedSession: (value: string | null) => void;
|
||||
setIsNewChatDialogOpen: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export function SessionList({
|
||||
sessions,
|
||||
agents,
|
||||
selectedSession,
|
||||
isLoading,
|
||||
searchTerm,
|
||||
selectedAgentFilter,
|
||||
showAgentFilter,
|
||||
setSearchTerm,
|
||||
setSelectedAgentFilter,
|
||||
setShowAgentFilter,
|
||||
setSelectedSession,
|
||||
setIsNewChatDialogOpen,
|
||||
}: SessionListProps) {
|
||||
const filteredSessions = sessions.filter((session) => {
|
||||
const matchesSearchTerm = session.id
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase());
|
||||
|
||||
if (selectedAgentFilter === "all") {
|
||||
return matchesSearchTerm;
|
||||
}
|
||||
|
||||
const sessionAgentId = session.id.split("_")[1];
|
||||
return matchesSearchTerm && sessionAgentId === selectedAgentFilter;
|
||||
});
|
||||
|
||||
const sortedSessions = [...filteredSessions].sort((a, b) => {
|
||||
const updateTimeA = new Date(a.update_time).getTime();
|
||||
const updateTimeB = new Date(b.update_time).getTime();
|
||||
|
||||
return updateTimeB - updateTimeA;
|
||||
});
|
||||
|
||||
const formatDateTime = (dateTimeStr: string) => {
|
||||
try {
|
||||
const date = new Date(dateTimeStr);
|
||||
|
||||
const day = date.getDate().toString().padStart(2, "0");
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
const year = date.getFullYear();
|
||||
const hours = date.getHours().toString().padStart(2, "0");
|
||||
const minutes = date.getMinutes().toString().padStart(2, "0");
|
||||
|
||||
return `${day}/${month}/${year} ${hours}:${minutes}`;
|
||||
} catch (error) {
|
||||
return "Invalid date";
|
||||
}
|
||||
};
|
||||
|
||||
const getExternalId = (sessionId: string) => {
|
||||
return sessionId.split("_")[0];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-64 border-r border-neutral-700 flex flex-col bg-neutral-900">
|
||||
<div className="p-4 border-b border-neutral-700">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Button
|
||||
onClick={() => setIsNewChatDialogOpen(true)}
|
||||
className="bg-emerald-800 text-emerald-100 hover:bg-emerald-700 border-emerald-700"
|
||||
size="sm"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" /> New Conversation
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-neutral-500" />
|
||||
<Input
|
||||
placeholder="Search conversations..."
|
||||
className="pl-9 bg-neutral-800 border-neutral-700 text-neutral-200 focus-visible:ring-emerald-500"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-neutral-400 hover:text-white hover:bg-neutral-800"
|
||||
onClick={() => setShowAgentFilter(!showAgentFilter)}
|
||||
>
|
||||
<Filter className="h-4 w-4 mr-1" />
|
||||
Filter
|
||||
</Button>
|
||||
|
||||
{selectedAgentFilter !== "all" && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedAgentFilter("all")}
|
||||
className="text-neutral-400 hover:text-white hover:bg-neutral-800"
|
||||
>
|
||||
Clear filter
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showAgentFilter && (
|
||||
<div className="pt-1">
|
||||
<Select
|
||||
value={selectedAgentFilter}
|
||||
onValueChange={setSelectedAgentFilter}
|
||||
>
|
||||
<SelectTrigger className="bg-neutral-800 border-neutral-700 text-neutral-200">
|
||||
<SelectValue placeholder="Filter by agent" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-neutral-900 border-neutral-700 text-white">
|
||||
<SelectItem
|
||||
value="all"
|
||||
className="data-[selected]:bg-neutral-800 data-[highlighted]:bg-neutral-800 !text-white hover:text-emerald-400 data-[selected]:!text-emerald-400"
|
||||
>
|
||||
All agents
|
||||
</SelectItem>
|
||||
{agents.map((agent) => (
|
||||
<SelectItem
|
||||
key={agent.id}
|
||||
value={agent.id}
|
||||
className="data-[selected]:bg-neutral-800 data-[highlighted]:bg-neutral-800 !text-white hover:text-emerald-400 data-[selected]:!text-emerald-400"
|
||||
>
|
||||
{agent.name.slice(0, 15)}{" "}
|
||||
{agent.name.length > 15 && "..."}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center items-center h-24">
|
||||
<Loader2 className="h-5 w-5 text-emerald-400 animate-spin" />
|
||||
</div>
|
||||
) : sortedSessions.length > 0 ? (
|
||||
<div className="px-4 pt-2 space-y-2">
|
||||
{sortedSessions.map((session) => {
|
||||
const agentId = session.id.split("_")[1];
|
||||
const agentInfo = agents.find((a) => a.id === agentId);
|
||||
const externalId = getExternalId(session.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={session.id}
|
||||
className={`p-3 rounded-md cursor-pointer transition-colors group relative ${
|
||||
selectedSession === session.id
|
||||
? "bg-emerald-800/20 border border-emerald-600/40"
|
||||
: "bg-neutral-800 hover:bg-neutral-700 border border-transparent"
|
||||
}`}
|
||||
onClick={() => setSelectedSession(session.id)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-500 mr-2"></div>
|
||||
<div className="text-neutral-200 font-medium truncate max-w-[180px]">
|
||||
{externalId}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
{agentInfo && (
|
||||
<Badge className="bg-neutral-700 text-emerald-400 border-neutral-600 text-xs">
|
||||
{agentInfo.name.slice(0, 15)}
|
||||
{agentInfo.name.length > 15 && "..."}
|
||||
</Badge>
|
||||
)}
|
||||
<div className="text-xs text-neutral-500 ml-auto">
|
||||
{formatDateTime(session.update_time)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : searchTerm || selectedAgentFilter !== "all" ? (
|
||||
<div className="text-center py-4 text-neutral-400">
|
||||
No results found
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-4 text-neutral-400">
|
||||
Click "New" to start
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
881
frontend/app/chat/page.tsx
Normal file
881
frontend/app/chat/page.tsx
Normal file
@@ -0,0 +1,881 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/chat/page.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import {
|
||||
MessageSquare,
|
||||
Send,
|
||||
Plus,
|
||||
Search,
|
||||
Loader2,
|
||||
X,
|
||||
Trash2,
|
||||
Bot,
|
||||
} from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogHeader,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { listAgents } from "@/services/agentService";
|
||||
import {
|
||||
listSessions,
|
||||
getSessionMessages,
|
||||
ChatMessage,
|
||||
deleteSession,
|
||||
ChatSession,
|
||||
ChatPart
|
||||
} from "@/services/sessionService";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useAgentWebSocket } from "@/hooks/use-agent-webSocket";
|
||||
import { getAccessTokenFromCookie } from "@/lib/utils";
|
||||
import { ChatMessage as ChatMessageComponent } from "./components/ChatMessage";
|
||||
import { SessionList } from "./components/SessionList";
|
||||
import { ChatInput } from "./components/ChatInput";
|
||||
import { FileData } from "@/lib/file-utils";
|
||||
import { AgentInfoDialog } from "./components/AgentInfoDialog";
|
||||
|
||||
interface FunctionMessageContent {
|
||||
title: string;
|
||||
content: string;
|
||||
author?: string;
|
||||
}
|
||||
|
||||
export default function Chat() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [agents, setAgents] = useState<any[]>([]);
|
||||
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [agentSearchTerm, setAgentSearchTerm] = useState("");
|
||||
const [selectedAgentFilter, setSelectedAgentFilter] = useState<string>("all");
|
||||
const [messageInput, setMessageInput] = useState("");
|
||||
const [selectedSession, setSelectedSession] = useState<string | null>(null);
|
||||
const [currentAgentId, setCurrentAgentId] = useState<string | null>(null);
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const [isNewChatDialogOpen, setIsNewChatDialogOpen] = useState(false);
|
||||
const [showAgentFilter, setShowAgentFilter] = useState(false);
|
||||
const [expandedFunctions, setExpandedFunctions] = useState<
|
||||
Record<string, boolean>
|
||||
>({});
|
||||
const [isAgentInfoDialogOpen, setIsAgentInfoDialogOpen] = useState(false);
|
||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||
const { toast } = useToast();
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
|
||||
const user =
|
||||
typeof window !== "undefined"
|
||||
? JSON.parse(localStorage.getItem("user") || "{}")
|
||||
: {};
|
||||
const clientId = user?.client_id || "test";
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (messagesContainerRef.current) {
|
||||
messagesContainerRef.current.scrollTop =
|
||||
messagesContainerRef.current.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const agentsResponse = await listAgents(clientId);
|
||||
setAgents(agentsResponse.data);
|
||||
|
||||
const sessionsResponse = await listSessions(clientId);
|
||||
setSessions(sessionsResponse.data);
|
||||
} catch (error) {
|
||||
console.error("Error loading data:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [clientId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedSession) {
|
||||
setMessages([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadMessages = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await getSessionMessages(selectedSession);
|
||||
setMessages(response.data);
|
||||
|
||||
const agentId = selectedSession.split("_")[1];
|
||||
setCurrentAgentId(agentId);
|
||||
|
||||
setTimeout(scrollToBottom, 100);
|
||||
} catch (error) {
|
||||
console.error("Error loading messages:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadMessages();
|
||||
}, [selectedSession]);
|
||||
|
||||
useEffect(() => {
|
||||
if (messages.length > 0) {
|
||||
setTimeout(scrollToBottom, 100);
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
const filteredSessions = sessions.filter((session) => {
|
||||
const matchesSearchTerm = session.id
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase());
|
||||
|
||||
if (selectedAgentFilter === "all") {
|
||||
return matchesSearchTerm;
|
||||
}
|
||||
|
||||
const sessionAgentId = session.id.split("_")[1];
|
||||
return matchesSearchTerm && sessionAgentId === selectedAgentFilter;
|
||||
});
|
||||
|
||||
const sortedSessions = [...filteredSessions].sort((a, b) => {
|
||||
const updateTimeA = new Date(a.update_time).getTime();
|
||||
const updateTimeB = new Date(b.update_time).getTime();
|
||||
|
||||
return updateTimeB - updateTimeA;
|
||||
});
|
||||
|
||||
const formatDateTime = (dateTimeStr: string) => {
|
||||
try {
|
||||
const date = new Date(dateTimeStr);
|
||||
|
||||
const day = date.getDate().toString().padStart(2, "0");
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
const year = date.getFullYear();
|
||||
const hours = date.getHours().toString().padStart(2, "0");
|
||||
const minutes = date.getMinutes().toString().padStart(2, "0");
|
||||
|
||||
return `${day}/${month}/${year} ${hours}:${minutes}`;
|
||||
} catch (error) {
|
||||
return "Invalid date";
|
||||
}
|
||||
};
|
||||
|
||||
const filteredAgents = agents.filter(
|
||||
(agent) =>
|
||||
agent.name.toLowerCase().includes(agentSearchTerm.toLowerCase()) ||
|
||||
(agent.description &&
|
||||
agent.description.toLowerCase().includes(agentSearchTerm.toLowerCase()))
|
||||
);
|
||||
|
||||
const selectAgent = (agentId: string) => {
|
||||
setCurrentAgentId(agentId);
|
||||
setSelectedSession(null);
|
||||
setMessages([]);
|
||||
setIsNewChatDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleSendMessage = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!messageInput.trim() || !currentAgentId) return;
|
||||
setIsSending(true);
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: `temp-${Date.now()}`,
|
||||
content: {
|
||||
parts: [{ text: messageInput }],
|
||||
role: "user",
|
||||
},
|
||||
author: "user",
|
||||
timestamp: Date.now() / 1000,
|
||||
},
|
||||
]);
|
||||
wsSendMessage(messageInput);
|
||||
setMessageInput("");
|
||||
const textarea = document.querySelector("textarea");
|
||||
if (textarea) textarea.style.height = "auto";
|
||||
};
|
||||
|
||||
const handleSendMessageWithFiles = (message: string, files?: FileData[]) => {
|
||||
if ((!message.trim() && (!files || files.length === 0)) || !currentAgentId)
|
||||
return;
|
||||
setIsSending(true);
|
||||
|
||||
const messageParts: ChatPart[] = [];
|
||||
|
||||
if (message.trim()) {
|
||||
messageParts.push({ text: message });
|
||||
}
|
||||
|
||||
if (files && files.length > 0) {
|
||||
files.forEach(file => {
|
||||
messageParts.push({
|
||||
inline_data: {
|
||||
data: file.data,
|
||||
mime_type: file.content_type,
|
||||
metadata: {
|
||||
filename: file.filename
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: `temp-${Date.now()}`,
|
||||
content: {
|
||||
parts: messageParts,
|
||||
role: "user"
|
||||
},
|
||||
author: "user",
|
||||
timestamp: Date.now() / 1000,
|
||||
},
|
||||
]);
|
||||
|
||||
wsSendMessage(message, files);
|
||||
|
||||
setMessageInput("");
|
||||
const textarea = document.querySelector("textarea");
|
||||
if (textarea) textarea.style.height = "auto";
|
||||
};
|
||||
|
||||
const generateExternalId = () => {
|
||||
const now = new Date();
|
||||
return (
|
||||
now.getFullYear().toString() +
|
||||
(now.getMonth() + 1).toString().padStart(2, "0") +
|
||||
now.getDate().toString().padStart(2, "0") +
|
||||
now.getHours().toString().padStart(2, "0") +
|
||||
now.getMinutes().toString().padStart(2, "0") +
|
||||
now.getSeconds().toString().padStart(2, "0") +
|
||||
now.getMilliseconds().toString().padStart(3, "0")
|
||||
);
|
||||
};
|
||||
|
||||
const currentAgent = agents.find((agent) => agent.id === currentAgentId);
|
||||
|
||||
const getCurrentSessionInfo = () => {
|
||||
if (!selectedSession) return null;
|
||||
|
||||
const parts = selectedSession.split("_");
|
||||
|
||||
try {
|
||||
const dateStr = parts[0];
|
||||
if (dateStr.length >= 8) {
|
||||
const year = dateStr.substring(0, 4);
|
||||
const month = dateStr.substring(4, 6);
|
||||
const day = dateStr.substring(6, 8);
|
||||
|
||||
return {
|
||||
externalId: parts[0],
|
||||
agentId: parts[1],
|
||||
displayDate: `${day}/${month}/${year}`,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error processing session ID:", e);
|
||||
}
|
||||
|
||||
return {
|
||||
externalId: parts[0],
|
||||
agentId: parts[1],
|
||||
displayDate: "Session",
|
||||
};
|
||||
};
|
||||
|
||||
const getExternalId = (sessionId: string) => {
|
||||
return sessionId.split("_")[0];
|
||||
};
|
||||
|
||||
const containsMarkdown = (text: string): boolean => {
|
||||
if (!text || text.length < 3) return false;
|
||||
|
||||
const markdownPatterns = [
|
||||
/[*_]{1,2}[^*_]+[*_]{1,2}/, // bold/italic
|
||||
/\[[^\]]+\]\([^)]+\)/, // links
|
||||
/^#{1,6}\s/m, // headers
|
||||
/^[-*+]\s/m, // unordered lists
|
||||
/^[0-9]+\.\s/m, // ordered lists
|
||||
/^>\s/m, // block quotes
|
||||
/`[^`]+`/, // inline code
|
||||
/```[\s\S]*?```/, // code blocks
|
||||
/^\|(.+\|)+$/m, // tables
|
||||
/!\[[^\]]*\]\([^)]+\)/, // images
|
||||
];
|
||||
|
||||
return markdownPatterns.some((pattern) => pattern.test(text));
|
||||
};
|
||||
|
||||
const getMessageText = (
|
||||
message: ChatMessage
|
||||
): string | FunctionMessageContent => {
|
||||
const author = message.author;
|
||||
const parts = message.content.parts;
|
||||
|
||||
if (!parts || parts.length === 0) return "Empty content";
|
||||
|
||||
const functionCallPart = parts.find(
|
||||
(part) => part.functionCall || part.function_call
|
||||
);
|
||||
const functionResponsePart = parts.find(
|
||||
(part) => part.functionResponse || part.function_response
|
||||
);
|
||||
|
||||
const inlineDataParts = parts.filter((part) => part.inline_data);
|
||||
|
||||
if (functionCallPart) {
|
||||
const funcCall =
|
||||
functionCallPart.functionCall || functionCallPart.function_call || {};
|
||||
const args = funcCall.args || {};
|
||||
const name = funcCall.name || "unknown";
|
||||
const id = funcCall.id || "no-id";
|
||||
|
||||
return {
|
||||
author,
|
||||
title: `📞 Function call: ${name}`,
|
||||
content: `ID: ${id}
|
||||
Args: ${
|
||||
Object.keys(args).length > 0
|
||||
? `\n${JSON.stringify(args, null, 2)}`
|
||||
: "{}"
|
||||
}`,
|
||||
} as FunctionMessageContent;
|
||||
}
|
||||
|
||||
if (functionResponsePart) {
|
||||
const funcResponse =
|
||||
functionResponsePart.functionResponse ||
|
||||
functionResponsePart.function_response ||
|
||||
{};
|
||||
const response = funcResponse.response || {};
|
||||
const name = funcResponse.name || "unknown";
|
||||
const id = funcResponse.id || "no-id";
|
||||
const status = response.status || "unknown";
|
||||
const statusEmoji = status === "error" ? "❌" : "✅";
|
||||
|
||||
let resultText = "";
|
||||
if (status === "error") {
|
||||
resultText = `Error: ${response.error_message || "Unknown error"}`;
|
||||
} else if (response.report) {
|
||||
resultText = `Result: ${response.report}`;
|
||||
} else if (response.result && response.result.content) {
|
||||
const content = response.result.content;
|
||||
if (Array.isArray(content) && content.length > 0 && content[0].text) {
|
||||
try {
|
||||
const textContent = content[0].text;
|
||||
const parsedJson = JSON.parse(textContent);
|
||||
resultText = `Result: \n${JSON.stringify(parsedJson, null, 2)}`;
|
||||
} catch (e) {
|
||||
resultText = `Result: ${content[0].text}`;
|
||||
}
|
||||
} else {
|
||||
resultText = `Result:\n${JSON.stringify(response, null, 2)}`;
|
||||
}
|
||||
} else {
|
||||
resultText = `Result:\n${JSON.stringify(response, null, 2)}`;
|
||||
}
|
||||
|
||||
return {
|
||||
author,
|
||||
title: `${statusEmoji} Function response: ${name}`,
|
||||
content: `ID: ${id}\n${resultText}`,
|
||||
} as FunctionMessageContent;
|
||||
}
|
||||
|
||||
if (parts.length === 1 && parts[0].text) {
|
||||
return {
|
||||
author,
|
||||
content: parts[0].text,
|
||||
title: "Message",
|
||||
} as FunctionMessageContent;
|
||||
}
|
||||
|
||||
const textParts = parts
|
||||
.filter((part) => part.text)
|
||||
.map((part) => part.text)
|
||||
.filter((text) => text);
|
||||
|
||||
if (textParts.length > 0) {
|
||||
return {
|
||||
author,
|
||||
content: textParts.join("\n\n"),
|
||||
title: "Message",
|
||||
} as FunctionMessageContent;
|
||||
}
|
||||
|
||||
return "Empty content";
|
||||
};
|
||||
|
||||
const toggleFunctionExpansion = (messageId: string) => {
|
||||
setExpandedFunctions((prev) => ({
|
||||
...prev,
|
||||
[messageId]: !prev[messageId],
|
||||
}));
|
||||
};
|
||||
|
||||
const agentColors: Record<string, string> = {
|
||||
Assistant: "bg-emerald-400",
|
||||
Programmer: "bg-[#00cc7d]",
|
||||
Writer: "bg-[#00b8ff]",
|
||||
Researcher: "bg-[#ff9d00]",
|
||||
Planner: "bg-[#9d00ff]",
|
||||
default: "bg-[#333]",
|
||||
};
|
||||
|
||||
const getAgentColor = (agentName: string) => {
|
||||
return agentColors[agentName] || agentColors.default;
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage(e as unknown as React.FormEvent);
|
||||
}
|
||||
};
|
||||
|
||||
const autoResizeTextarea = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const textarea = e.target;
|
||||
|
||||
textarea.style.height = "auto";
|
||||
|
||||
const maxHeight = 10 * 24;
|
||||
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
|
||||
|
||||
textarea.style.height = `${newHeight}px`;
|
||||
|
||||
setMessageInput(textarea.value);
|
||||
};
|
||||
|
||||
const handleDeleteSession = async () => {
|
||||
if (!selectedSession) return;
|
||||
|
||||
try {
|
||||
await deleteSession(selectedSession);
|
||||
|
||||
setSessions(sessions.filter((session) => session.id !== selectedSession));
|
||||
setSelectedSession(null);
|
||||
setMessages([]);
|
||||
setCurrentAgentId(null);
|
||||
setIsDeleteDialogOpen(false);
|
||||
|
||||
toast({
|
||||
title: "Session deleted successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting session:", error);
|
||||
toast({
|
||||
title: "Error deleting session",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onEvent = useCallback((event: any) => {
|
||||
setMessages((prev) => [...prev, event]);
|
||||
}, []);
|
||||
|
||||
const onTurnComplete = useCallback(() => {
|
||||
setIsSending(false);
|
||||
}, []);
|
||||
|
||||
const handleAgentInfoClick = () => {
|
||||
setIsAgentInfoDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleAgentUpdated = (updatedAgent: any) => {
|
||||
setAgents(agents.map(agent =>
|
||||
agent.id === updatedAgent.id ? updatedAgent : agent
|
||||
));
|
||||
|
||||
toast({
|
||||
title: "Agent updated successfully",
|
||||
description: "The agent has been updated with the new settings.",
|
||||
});
|
||||
};
|
||||
|
||||
const jwt = getAccessTokenFromCookie();
|
||||
|
||||
const agentId = useMemo(() => currentAgentId || "", [currentAgentId]);
|
||||
const externalId = useMemo(
|
||||
() =>
|
||||
selectedSession ? getExternalId(selectedSession) : generateExternalId(),
|
||||
[selectedSession]
|
||||
);
|
||||
|
||||
const { sendMessage: wsSendMessage, disconnect: _ } = useAgentWebSocket({
|
||||
agentId,
|
||||
externalId,
|
||||
jwt,
|
||||
onEvent,
|
||||
onTurnComplete,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex h-screen max-h-screen bg-[#121212]">
|
||||
<SessionList
|
||||
sessions={sessions}
|
||||
agents={agents}
|
||||
selectedSession={selectedSession}
|
||||
isLoading={isLoading}
|
||||
searchTerm={searchTerm}
|
||||
selectedAgentFilter={selectedAgentFilter}
|
||||
showAgentFilter={showAgentFilter}
|
||||
setSearchTerm={setSearchTerm}
|
||||
setSelectedAgentFilter={setSelectedAgentFilter}
|
||||
setShowAgentFilter={setShowAgentFilter}
|
||||
setSelectedSession={setSelectedSession}
|
||||
setIsNewChatDialogOpen={setIsNewChatDialogOpen}
|
||||
/>
|
||||
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{selectedSession || currentAgentId ? (
|
||||
<>
|
||||
<div className="p-4 border-b border-[#333] bg-neutral-900 shadow-md">
|
||||
{(() => {
|
||||
const sessionInfo = getCurrentSessionInfo();
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<div className="p-1 rounded-full bg-emerald-500/20">
|
||||
<MessageSquare className="h-5 w-5 text-emerald-400" />
|
||||
</div>
|
||||
{selectedSession
|
||||
? `Session ${
|
||||
sessionInfo?.externalId || selectedSession
|
||||
}`
|
||||
: "New Conversation"}
|
||||
</h2>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{currentAgent && (
|
||||
<Badge
|
||||
className="bg-emerald-500 text-white px-3 py-1 text-sm border-0 cursor-pointer hover:bg-emerald-600 transition-colors"
|
||||
onClick={handleAgentInfoClick}
|
||||
>
|
||||
{currentAgent.name || currentAgentId}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{selectedSession && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-neutral-400 hover:text-red-500 hover:bg-[#333]"
|
||||
onClick={() => setIsDeleteDialogOpen(true)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={messagesContainerRef}
|
||||
className="flex-1 overflow-y-auto p-4 bg-neutral-950"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-emerald-500 to-emerald-700 flex items-center justify-center shadow-lg mb-4 relative">
|
||||
<Loader2 className="h-6 w-6 text-white animate-spin" />
|
||||
<div className="absolute inset-0 rounded-full blur-md bg-emerald-400/20 animate-pulse"></div>
|
||||
</div>
|
||||
<p className="text-neutral-400 mb-2">Loading conversation...</p>
|
||||
</div>
|
||||
) : messages.length === 0 ? (
|
||||
<div className="flex flex-col h-full items-center justify-center text-center p-6">
|
||||
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-emerald-500/20 to-emerald-700/20 flex items-center justify-center shadow-lg mb-5 border border-emerald-500/30">
|
||||
<MessageSquare className="h-6 w-6 text-emerald-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-neutral-300 mb-2">
|
||||
{currentAgent ? `Chat with ${currentAgent.name}` : "New Conversation"}
|
||||
</h3>
|
||||
<p className="text-neutral-500 text-sm max-w-md">
|
||||
Type your message below to start the conversation. This chat will help you interact with the agent and explore its capabilities.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 w-full max-w-full">
|
||||
{messages.map((message) => {
|
||||
const messageContent = getMessageText(message);
|
||||
const agentColor = getAgentColor(message.author);
|
||||
const isExpanded = expandedFunctions[message.id] || false;
|
||||
|
||||
return (
|
||||
<ChatMessageComponent
|
||||
key={message.id}
|
||||
message={message}
|
||||
agentColor={agentColor}
|
||||
isExpanded={isExpanded}
|
||||
toggleExpansion={toggleFunctionExpansion}
|
||||
containsMarkdown={containsMarkdown}
|
||||
messageContent={messageContent}
|
||||
sessionId={selectedSession as string}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{isSending && (
|
||||
<div className="flex justify-start">
|
||||
<div className="flex gap-3 max-w-[80%]">
|
||||
<Avatar
|
||||
className="bg-gradient-to-br from-purple-600 to-purple-800 shadow-md border-0"
|
||||
>
|
||||
<AvatarFallback className="bg-transparent">
|
||||
<Bot className="h-4 w-4 text-white" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="rounded-lg p-3 bg-gradient-to-br from-neutral-800 to-neutral-900 border border-neutral-700/50 shadow-md">
|
||||
<div className="flex space-x-2">
|
||||
<div className="h-2 w-2 rounded-full bg-emerald-400 animate-bounce"></div>
|
||||
<div className="h-2 w-2 rounded-full bg-emerald-400 animate-bounce [animation-delay:0.2s]"></div>
|
||||
<div className="h-2 w-2 rounded-full bg-emerald-400 animate-bounce [animation-delay:0.4s]"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-4 pt-4 pb-6 border-t border-[#333] bg-neutral-900 shadow-inner">
|
||||
{isSending && !isLoading && (
|
||||
<div className="px-4 py-2 mb-3 rounded-lg bg-neutral-800/50 border border-neutral-700/30 text-sm text-neutral-400 flex items-center shadow-sm">
|
||||
<div className="mr-2 relative">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin text-emerald-400" />
|
||||
<div className="absolute inset-0 blur-sm bg-emerald-400/20 rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
Agent is thinking...
|
||||
</div>
|
||||
)}
|
||||
<div className="rounded-lg shadow-md bg-neutral-800/20 border border-neutral-700/30 p-1">
|
||||
<ChatInput
|
||||
onSendMessage={handleSendMessageWithFiles}
|
||||
isLoading={isSending || isLoading}
|
||||
placeholder="Type your message..."
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||
<div className="w-20 h-20 rounded-full bg-emerald-500/20 flex items-center justify-center shadow-lg mb-6 border border-emerald-500/30">
|
||||
<MessageSquare className="h-10 w-10 text-emerald-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold text-white mb-3">
|
||||
Select a conversation
|
||||
</h2>
|
||||
<p className="text-neutral-400 mb-8 max-w-md">
|
||||
Choose an existing conversation or start a new one.
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => setIsNewChatDialogOpen(true)}
|
||||
className="bg-emerald-500 text-white hover:bg-emerald-600 px-6 py-6 h-auto shadow-md rounded-xl"
|
||||
>
|
||||
<Plus className="mr-2 h-5 w-5" />
|
||||
New Conversation
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Dialog open={isNewChatDialogOpen} onOpenChange={setIsNewChatDialogOpen}>
|
||||
<DialogContent className="bg-neutral-900 border-neutral-800 text-white shadow-xl">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="p-1.5 rounded-full bg-emerald-500/20">
|
||||
<MessageSquare className="h-5 w-5 text-emerald-400" />
|
||||
</div>
|
||||
<DialogTitle className="text-xl font-medium text-white">
|
||||
New Conversation
|
||||
</DialogTitle>
|
||||
</div>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
Select an agent to start a new conversation.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="relative">
|
||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-500">
|
||||
<Search className="h-4 w-4" />
|
||||
</div>
|
||||
<Input
|
||||
placeholder="Search agents..."
|
||||
className="pl-10 bg-neutral-800/40 border-neutral-700/50 text-white focus-visible:ring-emerald-500/50 focus-visible:border-emerald-500/50 shadow-inner rounded-xl"
|
||||
value={agentSearchTerm}
|
||||
onChange={(e) => setAgentSearchTerm(e.target.value)}
|
||||
/>
|
||||
{agentSearchTerm && (
|
||||
<button
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-neutral-400 hover:text-emerald-500 transition-colors"
|
||||
onClick={() => setAgentSearchTerm("")}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-neutral-300 mb-2">Choose an agent:</div>
|
||||
|
||||
<ScrollArea className="h-[300px] pr-2">
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-8">
|
||||
<div className="relative">
|
||||
<Loader2 className="h-8 w-8 text-emerald-400 animate-spin" />
|
||||
<div className="absolute inset-0 rounded-full blur-md bg-emerald-400/20 animate-pulse"></div>
|
||||
</div>
|
||||
<p className="text-neutral-400 mt-4">Loading agents...</p>
|
||||
</div>
|
||||
) : filteredAgents.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{filteredAgents.map((agent) => (
|
||||
<div
|
||||
key={agent.id}
|
||||
className="p-3 rounded-md cursor-pointer transition-all bg-neutral-800 hover:bg-neutral-800/90 border border-neutral-700/30 hover:border-emerald-500/30 shadow-sm hover:shadow-md group"
|
||||
onClick={() => selectAgent(agent.id)}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="p-1 rounded-full bg-emerald-500/20 group-hover:bg-emerald-500/30 transition-colors">
|
||||
<MessageSquare size={14} className="text-emerald-400" />
|
||||
</div>
|
||||
<span className="font-medium text-white group-hover:text-emerald-50">
|
||||
{agent.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-1">
|
||||
<Badge className="text-xs bg-neutral-800/60 text-emerald-400 border border-emerald-500/30">
|
||||
{agent.type}
|
||||
</Badge>
|
||||
{agent.model && (
|
||||
<span className="text-xs text-neutral-400">
|
||||
{agent.model}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{agent.description && (
|
||||
<p className="text-xs text-neutral-300 mt-2 line-clamp-2 group-hover:text-neutral-200">
|
||||
{agent.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : agentSearchTerm ? (
|
||||
<div className="text-center py-4 text-neutral-400">
|
||||
No agent found with "{agentSearchTerm}"
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-4 text-neutral-400">
|
||||
<p>No agents available</p>
|
||||
<p className="text-sm mt-2">
|
||||
Create agents in the Agent Management screen
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={() => setIsNewChatDialogOpen(false)}
|
||||
variant="outline"
|
||||
className="bg-neutral-800/40 border-neutral-700/50 text-neutral-300 hover:bg-neutral-700/50 hover:text-white hover:border-neutral-600"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<DialogContent className="bg-neutral-900 border-neutral-800 text-white shadow-xl">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="p-1.5 rounded-full bg-red-500/20">
|
||||
<Trash2 className="h-5 w-5 text-red-400" />
|
||||
</div>
|
||||
<DialogTitle className="text-xl font-medium text-white">
|
||||
Delete Session
|
||||
</DialogTitle>
|
||||
</div>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
Are you sure you want to delete this session? This action cannot
|
||||
be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button
|
||||
onClick={() => setIsDeleteDialogOpen(false)}
|
||||
variant="outline"
|
||||
className="bg-neutral-800/40 border-neutral-700/50 text-neutral-300 hover:bg-neutral-700/50 hover:text-white hover:border-neutral-600"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDeleteSession}
|
||||
className="bg-red-600 hover:bg-red-700 text-white border-0 shadow-md"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Agent Info Dialog */}
|
||||
<AgentInfoDialog
|
||||
agent={currentAgent}
|
||||
open={isAgentInfoDialogOpen}
|
||||
onOpenChange={setIsAgentInfoDialogOpen}
|
||||
onAgentUpdated={handleAgentUpdated}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
52
frontend/app/client-layout.tsx
Normal file
52
frontend/app/client-layout.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/client-layout.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { usePathname } from "next/navigation"
|
||||
import Sidebar from "@/components/sidebar"
|
||||
|
||||
export default function ClientLayout({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname()
|
||||
const isLoginPage = pathname === "/login"
|
||||
const isVerifyEmailPage = pathname.startsWith("/security/verify-email")
|
||||
const isResetPasswordPage = pathname.startsWith("/security/reset-password")
|
||||
const isSharedChatPage = pathname.startsWith("/shared-chat")
|
||||
|
||||
if (isLoginPage || isVerifyEmailPage || isResetPasswordPage || isSharedChatPage) {
|
||||
return children
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-[#121212]">
|
||||
<Sidebar />
|
||||
<main className="flex-1 overflow-auto">{children}</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
31
frontend/app/clients/loading.tsx
Normal file
31
frontend/app/clients/loading.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/clients/loading.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
export default function Loading() {
|
||||
return null
|
||||
}
|
||||
462
frontend/app/clients/page.tsx
Normal file
462
frontend/app/clients/page.tsx
Normal file
@@ -0,0 +1,462 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/clients/page.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import { Plus, MoreHorizontal, Edit, Trash2, Search, Users, UserPlus } from "lucide-react"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import {
|
||||
createClient,
|
||||
listClients,
|
||||
getClient,
|
||||
updateClient,
|
||||
deleteClient,
|
||||
impersonateClient,
|
||||
Client,
|
||||
} from "@/services/clientService"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
export default function ClientsPage() {
|
||||
const { toast } = useToast()
|
||||
const router = useRouter()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
|
||||
const [selectedClient, setSelectedClient] = useState<Client | null>(null)
|
||||
|
||||
const [clientData, setClientData] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
})
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
const [limit, setLimit] = useState(1000)
|
||||
const [total, setTotal] = useState(0)
|
||||
|
||||
const [clients, setClients] = useState<Client[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchClients = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const res = await listClients((page - 1) * limit, limit)
|
||||
setClients(res.data)
|
||||
setTotal(res.data.length)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error loading clients",
|
||||
description: "Unable to load clients.",
|
||||
variant: "destructive",
|
||||
})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
fetchClients()
|
||||
}, [page, limit])
|
||||
|
||||
const filteredClients = Array.isArray(clients)
|
||||
? clients.filter(
|
||||
(client) =>
|
||||
client.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
client.email.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
)
|
||||
: []
|
||||
|
||||
const handleAddClient = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
try {
|
||||
if (selectedClient) {
|
||||
await updateClient(selectedClient.id, clientData)
|
||||
toast({
|
||||
title: "Client updated",
|
||||
description: `${clientData.name} was updated successfully.`,
|
||||
})
|
||||
} else {
|
||||
await createClient({ ...clientData, password: "Password@123" })
|
||||
toast({
|
||||
title: "Client added",
|
||||
description: `${clientData.name} was added successfully.`,
|
||||
})
|
||||
}
|
||||
setIsDialogOpen(false)
|
||||
resetForm()
|
||||
const res = await listClients((page - 1) * limit, limit)
|
||||
setClients(res.data)
|
||||
setTotal(res.data.length)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Unable to save client. Please try again.",
|
||||
variant: "destructive",
|
||||
})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditClient = async (client: Client) => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const res = await getClient(client.id)
|
||||
setSelectedClient(res.data)
|
||||
setClientData({
|
||||
name: res.data.name,
|
||||
email: res.data.email,
|
||||
})
|
||||
setIsDialogOpen(true)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error searching client",
|
||||
description: "Unable to search client.",
|
||||
variant: "destructive",
|
||||
})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDeleteClient = async () => {
|
||||
if (!selectedClient) return
|
||||
setIsLoading(true)
|
||||
try {
|
||||
await deleteClient(selectedClient.id)
|
||||
toast({
|
||||
title: "Client deleted",
|
||||
description: `${selectedClient.name} was deleted successfully.`,
|
||||
})
|
||||
setIsDeleteDialogOpen(false)
|
||||
setSelectedClient(null)
|
||||
const res = await listClients((page - 1) * limit, limit)
|
||||
setClients(res.data)
|
||||
setTotal(res.data.length)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Unable to delete client. Please try again.",
|
||||
variant: "destructive",
|
||||
})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleImpersonateClient = async (client: Client) => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await impersonateClient(client.id)
|
||||
|
||||
const currentUser = localStorage.getItem("user")
|
||||
if (currentUser) {
|
||||
localStorage.setItem("adminUser", currentUser)
|
||||
}
|
||||
|
||||
const currentToken = document.cookie.match(/access_token=([^;]+)/)?.[1]
|
||||
if (currentToken) {
|
||||
localStorage.setItem("adminToken", currentToken)
|
||||
}
|
||||
|
||||
localStorage.setItem("isImpersonating", "true")
|
||||
localStorage.setItem("impersonatedClient", client.name)
|
||||
|
||||
document.cookie = `isImpersonating=true; path=/; max-age=${60 * 60 * 24 * 7}`
|
||||
document.cookie = `impersonatedClient=${encodeURIComponent(client.name)}; path=/; max-age=${60 * 60 * 24 * 7}`
|
||||
document.cookie = `access_token=${response.access_token}; path=/; max-age=${60 * 60 * 24 * 7}`
|
||||
|
||||
const userData = {
|
||||
...JSON.parse(localStorage.getItem("user") || "{}"),
|
||||
is_admin: false,
|
||||
client_id: client.id
|
||||
}
|
||||
localStorage.setItem("user", JSON.stringify(userData))
|
||||
document.cookie = `user=${encodeURIComponent(JSON.stringify(userData))}; path=/; max-age=${60 * 60 * 24 * 7}`
|
||||
|
||||
toast({
|
||||
title: "Impersonation mode activated",
|
||||
description: `You are viewing as ${client.name}`,
|
||||
})
|
||||
|
||||
router.push("/agents")
|
||||
} catch (error) {
|
||||
console.error("Error impersonating client:", error)
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Unable to impersonate client",
|
||||
variant: "destructive",
|
||||
})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setClientData({
|
||||
name: "",
|
||||
email: "",
|
||||
})
|
||||
setSelectedClient(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-3xl font-bold text-white">Client Management</h1>
|
||||
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button onClick={resetForm} className="bg-emerald-400 text-black hover:bg-[#00cc7d]">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Client
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[500px] bg-[#1a1a1a] border-[#333]">
|
||||
<form onSubmit={handleAddClient}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">{selectedClient ? "Edit Client" : "New Client"}</DialogTitle>
|
||||
<DialogDescription className="text-neutral-400">
|
||||
{selectedClient
|
||||
? "Edit the existing client information."
|
||||
: "Fill in the information to create a new client."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name" className="text-neutral-300">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={clientData.name}
|
||||
onChange={(e) => setClientData({ ...clientData, name: e.target.value })}
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
placeholder="Company name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-neutral-300">
|
||||
Email
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={clientData.email}
|
||||
onChange={(e) => setClientData({ ...clientData, email: e.target.value })}
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
placeholder="contact@company.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
className="border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" className="bg-emerald-400 text-black hover:bg-[#00cc7d]" disabled={isLoading}>
|
||||
{isLoading ? "Saving..." : selectedClient ? "Save Changes" : "Add Client"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<AlertDialogContent className="bg-[#1a1a1a] border-[#333] text-white">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Confirm delete</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-neutral-400">
|
||||
Are you sure you want to delete the client "{selectedClient?.name}"? This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel className="border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white">
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDeleteClient}
|
||||
className="bg-red-600 text-white hover:bg-red-700"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Deleting..." : "Delete"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
|
||||
<Card className="bg-[#1a1a1a] border-[#333] mb-6">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white text-lg">Search Clients</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-neutral-500" />
|
||||
<Input
|
||||
placeholder="Search by name or email..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="bg-[#222] border-[#444] text-white pl-10"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-[#1a1a1a] border-[#333]">
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-[#333] hover:bg-[#222]">
|
||||
<TableHead className="text-neutral-300">Name</TableHead>
|
||||
<TableHead className="text-neutral-300">Email</TableHead>
|
||||
<TableHead className="text-neutral-300">Created At</TableHead>
|
||||
<TableHead className="text-neutral-300">Users</TableHead>
|
||||
<TableHead className="text-neutral-300">Agents</TableHead>
|
||||
<TableHead className="text-neutral-300 text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredClients.length > 0 ? (
|
||||
filteredClients.map((client) => (
|
||||
<TableRow key={client.id} className="border-[#333] hover:bg-[#222]">
|
||||
<TableCell className="font-medium text-white">{client.name}</TableCell>
|
||||
<TableCell className="text-neutral-300">{client.email}</TableCell>
|
||||
<TableCell className="text-neutral-300">
|
||||
{new Date(client.created_at).toLocaleDateString("pt-BR")}
|
||||
</TableCell>
|
||||
<TableCell className="text-neutral-300">
|
||||
<div className="flex items-center">
|
||||
<Users className="h-4 w-4 mr-1 text-emerald-400" />
|
||||
{client.users_count ?? 0}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-neutral-300">{client.agents_count ?? 0}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0 text-neutral-300 hover:bg-[#333]">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="bg-[#222] border-[#444] text-white">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="bg-[#444]" />
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer hover:bg-[#333]"
|
||||
onClick={() => handleEditClient(client)}
|
||||
>
|
||||
<Edit className="mr-2 h-4 w-4 text-emerald-400" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer hover:bg-[#333]"
|
||||
onClick={() => handleImpersonateClient(client)}
|
||||
>
|
||||
<UserPlus className="mr-2 h-4 w-4 text-emerald-400" />
|
||||
Enter as client
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer hover:bg-[#333] text-red-500"
|
||||
onClick={() => {
|
||||
setSelectedClient(client)
|
||||
setIsDeleteDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="h-24 text-center text-neutral-500">
|
||||
No clients found.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end mt-4">
|
||||
<Button onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page === 1 || isLoading}>
|
||||
Previous
|
||||
</Button>
|
||||
<span className="mx-4 text-white">Page {page} of {Math.ceil(total / limit) || 1}</span>
|
||||
<Button onClick={() => setPage((p) => p + 1)} disabled={page * limit >= total || isLoading}>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
333
frontend/app/documentation/components/A2AComplianceCard.tsx
Normal file
333
frontend/app/documentation/components/A2AComplianceCard.tsx
Normal file
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/documentation/components/A2AComplianceCard.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import {
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Shield,
|
||||
Zap,
|
||||
FileText,
|
||||
Settings,
|
||||
Network,
|
||||
AlertCircle,
|
||||
ExternalLink,
|
||||
ChevronDown,
|
||||
ChevronUp
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
export function A2AComplianceCard() {
|
||||
const [showCoreFeatures, setShowCoreFeatures] = useState(false);
|
||||
const [showAdvancedFeatures, setShowAdvancedFeatures] = useState(false);
|
||||
|
||||
const implementedFeatures = [
|
||||
{
|
||||
name: "JSON-RPC 2.0 Protocol",
|
||||
status: "implemented",
|
||||
icon: CheckCircle2,
|
||||
description: "Full compliance with JSON-RPC 2.0 specification"
|
||||
},
|
||||
{
|
||||
name: "message/send Method",
|
||||
status: "implemented",
|
||||
icon: CheckCircle2,
|
||||
description: "Standard HTTP messaging with proper request/response format"
|
||||
},
|
||||
{
|
||||
name: "message/stream Method",
|
||||
status: "implemented",
|
||||
icon: CheckCircle2,
|
||||
description: "Real-time streaming via Server-Sent Events (SSE)"
|
||||
},
|
||||
{
|
||||
name: "tasks/get Method",
|
||||
status: "implemented",
|
||||
icon: CheckCircle2,
|
||||
description: "Task status querying and monitoring"
|
||||
},
|
||||
{
|
||||
name: "tasks/cancel Method",
|
||||
status: "implemented",
|
||||
icon: CheckCircle2,
|
||||
description: "Task cancellation support"
|
||||
},
|
||||
{
|
||||
name: "agent/authenticatedExtendedCard",
|
||||
status: "implemented",
|
||||
icon: CheckCircle2,
|
||||
description: "Agent discovery and capability enumeration"
|
||||
},
|
||||
{
|
||||
name: "File Upload Support",
|
||||
status: "implemented",
|
||||
icon: CheckCircle2,
|
||||
description: "Base64 file encoding with proper MIME type handling"
|
||||
},
|
||||
{
|
||||
name: "UUID v4 Message IDs",
|
||||
status: "implemented",
|
||||
icon: CheckCircle2,
|
||||
description: "Standards-compliant unique message identification"
|
||||
},
|
||||
{
|
||||
name: "Authentication Methods",
|
||||
status: "implemented",
|
||||
icon: CheckCircle2,
|
||||
description: "API Key and Bearer token authentication"
|
||||
},
|
||||
{
|
||||
name: "Task State Management",
|
||||
status: "implemented",
|
||||
icon: CheckCircle2,
|
||||
description: "Complete task lifecycle: submitted → working → completed/failed"
|
||||
},
|
||||
{
|
||||
name: "Artifact Handling",
|
||||
status: "implemented",
|
||||
icon: CheckCircle2,
|
||||
description: "Complex response data with structured artifacts"
|
||||
},
|
||||
{
|
||||
name: "CORS Compliance",
|
||||
status: "implemented",
|
||||
icon: CheckCircle2,
|
||||
description: "Proper cross-origin resource sharing configuration"
|
||||
},
|
||||
{
|
||||
name: "tasks/pushNotificationConfig/set",
|
||||
status: "implemented",
|
||||
icon: CheckCircle2,
|
||||
description: "Set push notification configuration for tasks"
|
||||
},
|
||||
{
|
||||
name: "tasks/pushNotificationConfig/get",
|
||||
status: "implemented",
|
||||
icon: CheckCircle2,
|
||||
description: "Get push notification configuration for tasks"
|
||||
},
|
||||
{
|
||||
name: "tasks/resubscribe",
|
||||
status: "implemented",
|
||||
icon: CheckCircle2,
|
||||
description: "Resubscribe to task updates and notifications"
|
||||
}
|
||||
];
|
||||
|
||||
const advancedFeatures = [
|
||||
{
|
||||
name: "Push Notifications",
|
||||
status: "implemented",
|
||||
icon: CheckCircle2,
|
||||
description: "A2A pushNotificationConfig methods and webhook support"
|
||||
},
|
||||
{
|
||||
name: "Multi-turn Conversations",
|
||||
status: "implemented",
|
||||
icon: CheckCircle2,
|
||||
description: "Context preservation via contextId field as per A2A specification"
|
||||
},
|
||||
{
|
||||
name: "Enhanced Error Diagnostics",
|
||||
status: "implemented",
|
||||
icon: AlertCircle,
|
||||
description: "Comprehensive A2A error analysis and troubleshooting guidance"
|
||||
}
|
||||
];
|
||||
|
||||
const implementedCount = implementedFeatures.filter(f => f.status === 'implemented').length;
|
||||
const totalFeatures = implementedFeatures.length + advancedFeatures.length;
|
||||
const partialCount = advancedFeatures.filter(f => f.status === 'partial').length;
|
||||
const advancedImplementedCount = advancedFeatures.filter(f => f.status === 'implemented').length;
|
||||
const totalImplementedCount = implementedCount + advancedImplementedCount;
|
||||
const completionPercentage = Math.round(((totalImplementedCount + (partialCount * 0.5)) / totalFeatures) * 100);
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'implemented': return 'text-green-400';
|
||||
case 'partial': return 'text-yellow-400';
|
||||
case 'planned': return 'text-blue-400';
|
||||
default: return 'text-neutral-400';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string, IconComponent: any) => {
|
||||
const colorClass = getStatusColor(status);
|
||||
return <IconComponent className={`h-4 w-4 ${colorClass}`} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-gradient-to-br from-emerald-500/5 to-blue-500/5 border-emerald-500/20 text-white">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-emerald-400 flex items-center">
|
||||
<Network className="h-5 w-5 mr-2" />
|
||||
A2A Specification Compliance
|
||||
</CardTitle>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span className="text-neutral-300">Implementation Progress</span>
|
||||
<span className="text-emerald-400 font-medium">{completionPercentage}%</span>
|
||||
</div>
|
||||
<Progress value={completionPercentage} className="h-2" />
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
const shouldExpand = !showCoreFeatures || !showAdvancedFeatures;
|
||||
setShowCoreFeatures(shouldExpand);
|
||||
setShowAdvancedFeatures(shouldExpand);
|
||||
}}
|
||||
className="text-xs text-neutral-400 hover:text-white transition-colors px-2 py-1 rounded border border-neutral-600 hover:border-neutral-400"
|
||||
>
|
||||
{showCoreFeatures && showAdvancedFeatures ? 'Collapse All' : 'Expand All'}
|
||||
</button>
|
||||
<Badge className="bg-emerald-500/20 text-emerald-400 border-emerald-500/30">
|
||||
v0.2.0 Compatible
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex justify-center">
|
||||
<a
|
||||
href="https://google.github.io/A2A/specification"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center bg-blue-500/10 hover:bg-blue-500/20 px-4 py-2 rounded-lg border border-blue-500/20 transition-colors"
|
||||
>
|
||||
<FileText className="h-4 w-4 mr-2 text-blue-400" />
|
||||
<span className="text-blue-400">View Official Specification</span>
|
||||
<ExternalLink className="h-3 w-3 ml-2 text-blue-400" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer hover:bg-[#222]/30 p-2 rounded-lg transition-colors mb-4 border border-transparent hover:border-[#333]"
|
||||
onClick={() => setShowCoreFeatures(!showCoreFeatures)}
|
||||
>
|
||||
<h3 className="text-white font-semibold flex items-center">
|
||||
<CheckCircle2 className="h-4 w-4 mr-2 text-green-400" />
|
||||
Core Features
|
||||
<span className="ml-2 text-green-400 text-sm">({implementedCount}/{implementedFeatures.length} implemented)</span>
|
||||
</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-xs text-neutral-500">
|
||||
{showCoreFeatures ? 'Hide details' : 'Show details'}
|
||||
</span>
|
||||
{showCoreFeatures ? (
|
||||
<ChevronUp className="h-4 w-4 text-neutral-400 hover:text-white transition-colors" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4 text-neutral-400 hover:text-white transition-colors" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showCoreFeatures && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{implementedFeatures.map((feature, index) => (
|
||||
<div key={index} className="flex items-start space-x-3 bg-[#222]/50 p-3 rounded-lg border border-[#333]">
|
||||
{getStatusIcon(feature.status, feature.icon)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate">{feature.name}</p>
|
||||
<p className="text-xs text-neutral-400">{feature.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer hover:bg-[#222]/30 p-2 rounded-lg transition-colors mb-4 border border-transparent hover:border-[#333]"
|
||||
onClick={() => setShowAdvancedFeatures(!showAdvancedFeatures)}
|
||||
>
|
||||
<h3 className="text-white font-semibold flex items-center">
|
||||
<Settings className="h-4 w-4 mr-2 text-blue-400" />
|
||||
Advanced Features
|
||||
<span className="ml-2 text-blue-400 text-sm">({advancedImplementedCount}/{advancedFeatures.length} implemented)</span>
|
||||
</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-xs text-neutral-500">
|
||||
{showAdvancedFeatures ? 'Hide details' : 'Show details'}
|
||||
</span>
|
||||
{showAdvancedFeatures ? (
|
||||
<ChevronUp className="h-4 w-4 text-neutral-400 hover:text-white transition-colors" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4 text-neutral-400 hover:text-white transition-colors" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showAdvancedFeatures && (
|
||||
<div className="space-y-3">
|
||||
{advancedFeatures.map((feature, index) => (
|
||||
<div key={index} className="flex items-start space-x-3 bg-[#222]/50 p-3 rounded-lg border border-[#333]">
|
||||
{getStatusIcon(feature.status, feature.icon)}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-sm font-medium text-white">{feature.name}</p>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-xs ${
|
||||
feature.status === 'implemented' ? 'border-green-500 text-green-400' :
|
||||
feature.status === 'partial' ? 'border-yellow-500 text-yellow-400' :
|
||||
'border-blue-500 text-blue-400'
|
||||
}`}
|
||||
>
|
||||
{feature.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-400 mt-1">{feature.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-emerald-500/10 border border-emerald-500/20 rounded-lg p-4">
|
||||
<div className="flex items-start space-x-2">
|
||||
<Shield className="h-4 w-4 text-emerald-400 mt-0.5" />
|
||||
<div className="text-sm">
|
||||
<p className="text-emerald-400 font-medium">✓ 100% A2A v0.2.0 Compliance Achieved</p>
|
||||
<p className="text-emerald-300 mt-1">
|
||||
All 8 official RPC methods implemented • Complete protocol data objects • Full workflow support • Enterprise security ready
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
78
frontend/app/documentation/components/CodeBlock.tsx
Normal file
78
frontend/app/documentation/components/CodeBlock.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/documentation/components/CodeBlock.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
import React from "react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { dracula } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ClipboardCopy } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
interface CodeBlockProps {
|
||||
text: string;
|
||||
language: string;
|
||||
showLineNumbers?: boolean;
|
||||
}
|
||||
|
||||
export function CodeBlock({ text, language, showLineNumbers = true }: CodeBlockProps) {
|
||||
const { toast } = useToast();
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(text);
|
||||
toast({
|
||||
title: "Copied!",
|
||||
description: "Code copied to clipboard",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative rounded-md overflow-hidden">
|
||||
<SyntaxHighlighter
|
||||
language={language}
|
||||
style={dracula}
|
||||
showLineNumbers={showLineNumbers}
|
||||
wrapLines={true}
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: "1rem",
|
||||
borderRadius: "0.375rem",
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</SyntaxHighlighter>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2 text-white hover:bg-[#333] opacity-80 hover:opacity-100"
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
<ClipboardCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
317
frontend/app/documentation/components/CodeExamplesSection.tsx
Normal file
317
frontend/app/documentation/components/CodeExamplesSection.tsx
Normal file
@@ -0,0 +1,317 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/documentation/components/CodeExamplesSection.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { CodeBlock } from "@/app/documentation/components/CodeBlock";
|
||||
|
||||
interface CodeExamplesSectionProps {
|
||||
agentUrl: string;
|
||||
apiKey: string;
|
||||
jsonRpcRequest: any;
|
||||
curlExample: string;
|
||||
fetchExample: string;
|
||||
}
|
||||
|
||||
export function CodeExamplesSection({
|
||||
agentUrl,
|
||||
apiKey,
|
||||
jsonRpcRequest,
|
||||
curlExample,
|
||||
fetchExample
|
||||
}: CodeExamplesSectionProps) {
|
||||
const pythonExample = `import requests
|
||||
import json
|
||||
|
||||
def test_a2a_agent():
|
||||
url = "${agentUrl || "http://localhost:8000/api/v1/a2a/your-agent-id"}"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": "${apiKey || "your-api-key"}"
|
||||
}
|
||||
|
||||
payload = ${JSON.stringify(jsonRpcRequest, null, 2)}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
data = response.json()
|
||||
print("Agent response:", data)
|
||||
|
||||
return data
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_a2a_agent()`;
|
||||
|
||||
return (
|
||||
<Card className="bg-[#1a1a1a] border-[#333] text-white">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-emerald-400">Code Examples</CardTitle>
|
||||
<CardDescription className="text-neutral-400">
|
||||
Code snippets ready to use with A2A agents
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="curl">
|
||||
<TabsList className="bg-[#222] border-[#333] mb-4">
|
||||
<TabsTrigger value="curl" className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400">
|
||||
cURL
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="javascript" className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400">
|
||||
JavaScript
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="python" className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400">
|
||||
Python
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="curl" className="relative">
|
||||
<CodeBlock
|
||||
text={curlExample}
|
||||
language="bash"
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="javascript" className="relative">
|
||||
<CodeBlock
|
||||
text={fetchExample}
|
||||
language="javascript"
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="python" className="relative">
|
||||
<CodeBlock
|
||||
text={pythonExample}
|
||||
language="python"
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-8">
|
||||
<h3 className="text-xl font-semibold text-white mb-3">Sending files to the agent</h3>
|
||||
<p className="text-neutral-400 mb-4">
|
||||
You can attach files to messages sent to the agent using the A2A protocol.
|
||||
The files are encoded in base64 and incorporated into the message as parts of type "file".
|
||||
</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h4 className="text-lg font-medium text-emerald-400 mb-2">Python</h4>
|
||||
<CodeBlock
|
||||
text={`import asyncio
|
||||
import base64
|
||||
import os
|
||||
from uuid import uuid4
|
||||
|
||||
from common.client import A2ACardResolver, A2AClient
|
||||
|
||||
async def send_message_with_files():
|
||||
# Instantiate client
|
||||
card_resolver = A2ACardResolver("http://localhost:8000/api/v1/a2a/your-agent-id")
|
||||
card = card_resolver.get_agent_card()
|
||||
client = A2AClient(agent_card=card)
|
||||
|
||||
# Create session and task IDs
|
||||
session_id = uuid4().hex
|
||||
task_id = uuid4().hex
|
||||
|
||||
# Read file and encode in base64
|
||||
file_path = "example.jpg"
|
||||
with open(file_path, 'rb') as f:
|
||||
file_content = base64.b64encode(f.read()).decode('utf-8')
|
||||
file_name = os.path.basename(file_path)
|
||||
|
||||
# Create message with text and file
|
||||
message = {
|
||||
'role': 'user',
|
||||
'parts': [
|
||||
{
|
||||
'type': 'text',
|
||||
'text': 'Analyze this image for me',
|
||||
},
|
||||
{
|
||||
'type': 'file',
|
||||
'file': {
|
||||
'name': file_name,
|
||||
'bytes': file_content,
|
||||
'mime_type': 'application/octet-stream' # Important: include the mime_type for correct file processing
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
# Create request payload
|
||||
payload = {
|
||||
'id': task_id,
|
||||
'sessionId': session_id,
|
||||
'acceptedOutputModes': ['text'],
|
||||
'message': message,
|
||||
}
|
||||
|
||||
# Send request
|
||||
task_result = await client.send_task(payload)
|
||||
print(f'\\nResponse: {task_result.model_dump_json(exclude_none=True)}')
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(send_message_with_files())`}
|
||||
language="python"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-lg font-medium text-emerald-400 mb-2">JavaScript/TypeScript</h4>
|
||||
<CodeBlock
|
||||
text={`// Function to convert file to base64
|
||||
async function fileToBase64(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const result = reader.result;
|
||||
const base64 = result.split(',')[1];
|
||||
resolve(base64);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
async function sendMessageWithFiles() {
|
||||
// Select file (in a web application)
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const files = fileInput.files;
|
||||
|
||||
if (files.length === 0) {
|
||||
console.error('No file selected');
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert file to base64
|
||||
const file = files[0];
|
||||
const base64Data = await fileToBase64(file);
|
||||
|
||||
// Create session and task IDs
|
||||
const sessionId = crypto.randomUUID();
|
||||
const taskId = crypto.randomUUID();
|
||||
const callId = crypto.randomUUID();
|
||||
|
||||
// Create message with text and file
|
||||
const payload = {
|
||||
jsonrpc: "2.0",
|
||||
method: "message/send",
|
||||
params: {
|
||||
message: {
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Analyze this document for me",
|
||||
},
|
||||
{
|
||||
type: "file",
|
||||
file: {
|
||||
name: file.name,
|
||||
bytes: base64Data,
|
||||
mime_type: file.type
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
sessionId: sessionId,
|
||||
id: taskId,
|
||||
},
|
||||
id: callId,
|
||||
};
|
||||
|
||||
// Send request
|
||||
const response = await fetch('http://localhost:8000/api/v1/a2a/your-agent-id', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': 'your-api-key',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Agent response:', data);
|
||||
}`}
|
||||
language="javascript"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-lg font-medium text-emerald-400 mb-2">Curl</h4>
|
||||
<CodeBlock
|
||||
text={`# Convert file to base64
|
||||
FILE_PATH="example.jpg"
|
||||
FILE_NAME=$(basename $FILE_PATH)
|
||||
BASE64_CONTENT=$(base64 -w 0 $FILE_PATH)
|
||||
|
||||
# Create request payload
|
||||
read -r -d '' PAYLOAD << EOM
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "message/send",
|
||||
"params": {
|
||||
"message": {
|
||||
"role": "user",
|
||||
"parts": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Analyze this image for me"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"file": {
|
||||
"name": "$FILE_NAME",
|
||||
"bytes": "$BASE64_CONTENT",
|
||||
"mime_type": "$(file --mime-type -b $FILE_PATH)"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"sessionId": "session-123",
|
||||
"id": "task-456"
|
||||
},
|
||||
"id": "call-789"
|
||||
}
|
||||
EOM
|
||||
|
||||
# Send request
|
||||
curl -X POST \\
|
||||
http://localhost:8000/api/v1/a2a/your-agent-id \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
-H 'x-api-key: your-api-key' \\
|
||||
-d "$PAYLOAD"`}
|
||||
language="bash"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
588
frontend/app/documentation/components/DocumentationSection.tsx
Normal file
588
frontend/app/documentation/components/DocumentationSection.tsx
Normal file
@@ -0,0 +1,588 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/documentation/components/DocumentationSection.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
ClipboardCopy,
|
||||
Info,
|
||||
ExternalLink,
|
||||
Users,
|
||||
Shield,
|
||||
Zap,
|
||||
Network,
|
||||
FileText,
|
||||
MessageSquare,
|
||||
Settings,
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
Globe
|
||||
} from "lucide-react";
|
||||
import { CodeBlock } from "@/app/documentation/components/CodeBlock";
|
||||
|
||||
interface DocumentationSectionProps {
|
||||
copyToClipboard: (text: string) => void;
|
||||
}
|
||||
|
||||
export function DocumentationSection({ copyToClipboard }: DocumentationSectionProps) {
|
||||
const quickStartExample = {
|
||||
jsonrpc: "2.0",
|
||||
id: "req-001",
|
||||
method: "message/send",
|
||||
params: {
|
||||
message: {
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Hello! Can you help me analyze this data?"
|
||||
}
|
||||
],
|
||||
messageId: "6dbc13b5-bd57-4c2b-b503-24e381b6c8d6"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const streamingExample = {
|
||||
jsonrpc: "2.0",
|
||||
id: "req-002",
|
||||
method: "message/stream",
|
||||
params: {
|
||||
message: {
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Generate a detailed report on market trends"
|
||||
}
|
||||
],
|
||||
messageId: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fileUploadExample = {
|
||||
jsonrpc: "2.0",
|
||||
id: "req-003",
|
||||
method: "message/send",
|
||||
params: {
|
||||
message: {
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Analyze this image and highlight any faces."
|
||||
},
|
||||
{
|
||||
type: "file",
|
||||
file: {
|
||||
name: "input_image.png",
|
||||
mimeType: "image/png",
|
||||
bytes: "iVBORw0KGgoAAAANSUhEUgAAAAUA..."
|
||||
}
|
||||
}
|
||||
],
|
||||
messageId: "8f0dc03c-4c65-4a14-9b56-7e8b9f2d1a3c"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Hero Section */}
|
||||
<Card className="bg-gradient-to-br from-emerald-500/10 to-blue-500/10 border-emerald-500/20 text-white">
|
||||
<CardHeader className="text-center">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="flex items-center space-x-2 bg-emerald-500/20 px-4 py-2 rounded-full">
|
||||
<Network className="h-6 w-6 text-emerald-400" />
|
||||
<span className="font-bold text-emerald-400">Agent2Agent Protocol</span>
|
||||
</div>
|
||||
</div>
|
||||
<CardTitle className="text-3xl font-bold bg-gradient-to-r from-emerald-400 to-blue-400 bg-clip-text text-transparent">
|
||||
The Standard for AI Agent Communication
|
||||
</CardTitle>
|
||||
<p className="text-lg text-neutral-300 mt-4 max-w-3xl mx-auto">
|
||||
A2A is Google's open protocol enabling seamless communication and interoperability
|
||||
between AI agents across different platforms, providers, and architectures.
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap justify-center gap-4 mt-6">
|
||||
<a
|
||||
href="https://google.github.io/A2A/specification"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center bg-emerald-500/20 hover:bg-emerald-500/30 px-4 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
<FileText className="h-4 w-4 mr-2 text-emerald-400" />
|
||||
<span className="text-emerald-400">Official Specification</span>
|
||||
<ExternalLink className="h-3 w-3 ml-2 text-emerald-400" />
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/google/A2A"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center bg-blue-500/20 hover:bg-blue-500/30 px-4 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
<Globe className="h-4 w-4 mr-2 text-blue-400" />
|
||||
<span className="text-blue-400">GitHub Repository</span>
|
||||
<ExternalLink className="h-3 w-3 ml-2 text-blue-400" />
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Key Features */}
|
||||
<Card className="bg-[#1a1a1a] border-[#333] text-white">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-emerald-400 flex items-center">
|
||||
<Zap className="h-5 w-5 mr-2" />
|
||||
Key Features & Capabilities
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="bg-emerald-500/20 p-2 rounded-lg">
|
||||
<MessageSquare className="h-5 w-5 text-emerald-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-white">Multi-turn Conversations</h3>
|
||||
<p className="text-sm text-neutral-400">Support for complex, contextual dialogues between agents</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="bg-blue-500/20 p-2 rounded-lg">
|
||||
<FileText className="h-5 w-5 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-white">File Exchange</h3>
|
||||
<p className="text-sm text-neutral-400">Upload and download files with proper MIME type handling</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="bg-purple-500/20 p-2 rounded-lg">
|
||||
<Zap className="h-5 w-5 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-white">Real-time Streaming</h3>
|
||||
<p className="text-sm text-neutral-400">Server-Sent Events for live response streaming</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="bg-orange-500/20 p-2 rounded-lg">
|
||||
<Settings className="h-5 w-5 text-orange-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-white">Task Management</h3>
|
||||
<p className="text-sm text-neutral-400">Track, query, and cancel long-running tasks</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="bg-red-500/20 p-2 rounded-lg">
|
||||
<Shield className="h-5 w-5 text-red-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-white">Enterprise Security</h3>
|
||||
<p className="text-sm text-neutral-400">Bearer tokens, API keys, and HTTPS enforcement</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="bg-green-500/20 p-2 rounded-lg">
|
||||
<Users className="h-5 w-5 text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-white">Agent Discovery</h3>
|
||||
<p className="text-sm text-neutral-400">Standardized agent cards for capability discovery</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Protocol Methods */}
|
||||
<Card className="bg-[#1a1a1a] border-[#333] text-white">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-emerald-400">Protocol Methods</CardTitle>
|
||||
<p className="text-neutral-400">A2A supports multiple RPC methods for different interaction patterns</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="messaging" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3 bg-[#222] border-[#444]">
|
||||
<TabsTrigger value="messaging" className="data-[state=active]:bg-emerald-500/20 data-[state=active]:text-emerald-400">
|
||||
Messaging
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="tasks" className="data-[state=active]:bg-emerald-500/20 data-[state=active]:text-emerald-400">
|
||||
Task Management
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="discovery" className="data-[state=active]:bg-emerald-500/20 data-[state=active]:text-emerald-400">
|
||||
Discovery
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="messaging" className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-[#222] p-4 rounded-lg border border-[#444]">
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<Badge variant="outline" className="border-emerald-500 text-emerald-400">message/send</Badge>
|
||||
<CheckCircle2 className="h-4 w-4 text-emerald-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-white mb-2">Standard HTTP Request</h4>
|
||||
<p className="text-sm text-neutral-400 mb-3">
|
||||
Send a message and receive a complete response after processing is finished.
|
||||
</p>
|
||||
<ul className="text-xs text-neutral-400 space-y-1">
|
||||
<li>• Single request/response cycle</li>
|
||||
<li>• Best for simple queries</li>
|
||||
<li>• Lower complexity implementation</li>
|
||||
<li>• Synchronous operation</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#222] p-4 rounded-lg border border-[#444]">
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<Badge variant="outline" className="border-blue-500 text-blue-400">message/stream</Badge>
|
||||
<Zap className="h-4 w-4 text-blue-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-white mb-2">Real-time Streaming</h4>
|
||||
<p className="text-sm text-neutral-400 mb-3">
|
||||
Receive partial responses in real-time via Server-Sent Events.
|
||||
</p>
|
||||
<ul className="text-xs text-neutral-400 space-y-1">
|
||||
<li>• Progressive response delivery</li>
|
||||
<li>• Better UX for long tasks</li>
|
||||
<li>• Live status updates</li>
|
||||
<li>• Asynchronous operation</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="tasks" className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-[#222] p-4 rounded-lg border border-[#444]">
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<Badge variant="outline" className="border-purple-500 text-purple-400">tasks/get</Badge>
|
||||
<Settings className="h-4 w-4 text-purple-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-white mb-2">Query Task Status</h4>
|
||||
<p className="text-sm text-neutral-400 mb-3">
|
||||
Check the status, progress, and results of a specific task.
|
||||
</p>
|
||||
<ul className="text-xs text-neutral-400 space-y-1">
|
||||
<li>• Real-time status checking</li>
|
||||
<li>• Progress monitoring</li>
|
||||
<li>• Result retrieval</li>
|
||||
<li>• Error diagnosis</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#222] p-4 rounded-lg border border-[#444]">
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<Badge variant="outline" className="border-red-500 text-red-400">tasks/cancel</Badge>
|
||||
<AlertCircle className="h-4 w-4 text-red-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-white mb-2">Cancel Task</h4>
|
||||
<p className="text-sm text-neutral-400 mb-3">
|
||||
Terminate a running task before completion.
|
||||
</p>
|
||||
<ul className="text-xs text-neutral-400 space-y-1">
|
||||
<li>• Graceful task termination</li>
|
||||
<li>• Resource cleanup</li>
|
||||
<li>• Cost optimization</li>
|
||||
<li>• User control</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="discovery" className="space-y-4">
|
||||
<div className="bg-[#222] p-4 rounded-lg border border-[#444]">
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<Badge variant="outline" className="border-green-500 text-green-400">agent/authenticatedExtendedCard</Badge>
|
||||
<Users className="h-4 w-4 text-green-400" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-white mb-2">Agent Discovery</h4>
|
||||
<p className="text-sm text-neutral-400 mb-3">
|
||||
Retrieve detailed information about agent capabilities, skills, and requirements.
|
||||
</p>
|
||||
<ul className="text-xs text-neutral-400 space-y-1">
|
||||
<li>• Agent capability discovery</li>
|
||||
<li>• Skill and tool enumeration</li>
|
||||
<li>• Authentication requirements</li>
|
||||
<li>• API version compatibility</li>
|
||||
</ul>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Code Examples */}
|
||||
<Card className="bg-[#1a1a1a] border-[#333] text-white">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-emerald-400">Quick Start Examples</CardTitle>
|
||||
<p className="text-neutral-400">Ready-to-use JSON-RPC examples based on the official A2A specification</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="basic" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3 bg-[#222] border-[#444]">
|
||||
<TabsTrigger value="basic" className="data-[state=active]:bg-emerald-500/20 data-[state=active]:text-emerald-400">
|
||||
Basic Message
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="streaming" className="data-[state=active]:bg-emerald-500/20 data-[state=active]:text-emerald-400">
|
||||
Streaming
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="files" className="data-[state=active]:bg-emerald-500/20 data-[state=active]:text-emerald-400">
|
||||
File Upload
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="basic" className="space-y-4">
|
||||
<div className="relative">
|
||||
<CodeBlock
|
||||
text={JSON.stringify(quickStartExample, null, 2)}
|
||||
language="json"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||
onClick={() => copyToClipboard(JSON.stringify(quickStartExample, null, 2))}
|
||||
>
|
||||
<ClipboardCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4">
|
||||
<div className="flex items-start space-x-2">
|
||||
<Info className="h-4 w-4 text-blue-400 mt-0.5" />
|
||||
<div className="text-sm">
|
||||
<p className="text-blue-400 font-medium">Key Points:</p>
|
||||
<ul className="text-blue-300 mt-1 space-y-1">
|
||||
<li>• Uses <code className="bg-blue-500/20 px-1 rounded">message/send</code> for standard HTTP requests</li>
|
||||
<li>• <code className="bg-blue-500/20 px-1 rounded">messageId</code> must be a valid UUID v4</li>
|
||||
<li>• Response contains task ID, status, and artifacts</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="streaming" className="space-y-4">
|
||||
<div className="relative">
|
||||
<CodeBlock
|
||||
text={JSON.stringify(streamingExample, null, 2)}
|
||||
language="json"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||
onClick={() => copyToClipboard(JSON.stringify(streamingExample, null, 2))}
|
||||
>
|
||||
<ClipboardCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="bg-purple-500/10 border border-purple-500/20 rounded-lg p-4">
|
||||
<div className="flex items-start space-x-2">
|
||||
<Zap className="h-4 w-4 text-purple-400 mt-0.5" />
|
||||
<div className="text-sm">
|
||||
<p className="text-purple-400 font-medium">Streaming Features:</p>
|
||||
<ul className="text-purple-300 mt-1 space-y-1">
|
||||
<li>• Real-time Server-Sent Events (SSE)</li>
|
||||
<li>• Progressive content delivery</li>
|
||||
<li>• Status updates: submitted → working → completed</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="files" className="space-y-4">
|
||||
<div className="relative">
|
||||
<CodeBlock
|
||||
text={JSON.stringify(fileUploadExample, null, 2)}
|
||||
language="json"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||
onClick={() => copyToClipboard(JSON.stringify(fileUploadExample, null, 2))}
|
||||
>
|
||||
<ClipboardCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-4">
|
||||
<div className="flex items-start space-x-2">
|
||||
<FileText className="h-4 w-4 text-green-400 mt-0.5" />
|
||||
<div className="text-sm">
|
||||
<p className="text-green-400 font-medium">File Handling:</p>
|
||||
<ul className="text-green-300 mt-1 space-y-1">
|
||||
<li>• Support for multiple file types (images, documents, etc.)</li>
|
||||
<li>• Base64 encoding for binary data</li>
|
||||
<li>• Proper MIME type specification</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Security & Best Practices */}
|
||||
<Card className="bg-[#1a1a1a] border-[#333] text-white">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-emerald-400 flex items-center">
|
||||
<Shield className="h-5 w-5 mr-2" />
|
||||
Security & Best Practices
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h3 className="text-white font-semibold mb-3 flex items-center">
|
||||
<Shield className="h-4 w-4 mr-2 text-emerald-400" />
|
||||
Authentication Methods
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="bg-[#222] p-3 rounded-lg border border-[#444]">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<code className="text-emerald-400 text-sm">x-api-key</code>
|
||||
<Badge variant="outline" className="text-xs">Recommended</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-400">Custom header for API key authentication</p>
|
||||
</div>
|
||||
<div className="bg-[#222] p-3 rounded-lg border border-[#444]">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<code className="text-blue-400 text-sm">Authorization: Bearer</code>
|
||||
<Badge variant="outline" className="text-xs">Standard</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-400">OAuth 2.0 Bearer token authentication</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-white font-semibold mb-3 flex items-center">
|
||||
<AlertCircle className="h-4 w-4 mr-2 text-orange-400" />
|
||||
Security Requirements
|
||||
</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle2 className="h-3 w-3 text-green-400" />
|
||||
<span className="text-neutral-300">HTTPS/TLS encryption required</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle2 className="h-3 w-3 text-green-400" />
|
||||
<span className="text-neutral-300">Input validation on all parameters</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle2 className="h-3 w-3 text-green-400" />
|
||||
<span className="text-neutral-300">Rate limiting and resource controls</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle2 className="h-3 w-3 text-green-400" />
|
||||
<span className="text-neutral-300">Proper CORS configuration</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-500/10 border border-amber-500/20 rounded-lg p-4">
|
||||
<div className="flex items-start space-x-2">
|
||||
<AlertCircle className="h-4 w-4 text-amber-400 mt-0.5" />
|
||||
<div className="text-sm">
|
||||
<p className="text-amber-400 font-medium">Important:</p>
|
||||
<p className="text-amber-300 mt-1">
|
||||
Always obtain API credentials out-of-band. Never include sensitive authentication
|
||||
data in client-side code or version control systems.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* A2A vs MCP */}
|
||||
<Card className="bg-[#1a1a1a] border-[#333] text-white">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-emerald-400 flex items-center">
|
||||
<Network className="h-5 w-5 mr-2" />
|
||||
A2A vs Model Context Protocol (MCP)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-[#222] border-b border-[#444]">
|
||||
<th className="p-4 text-left text-neutral-300">Aspect</th>
|
||||
<th className="p-4 text-left text-emerald-400">Agent2Agent (A2A)</th>
|
||||
<th className="p-4 text-left text-blue-400">Model Context Protocol (MCP)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="border-b border-[#333]">
|
||||
<td className="p-4 text-neutral-300 font-medium">Purpose</td>
|
||||
<td className="p-4 text-neutral-300">Agent-to-agent communication</td>
|
||||
<td className="p-4 text-neutral-300">Model-to-tool/resource integration</td>
|
||||
</tr>
|
||||
<tr className="border-b border-[#333]">
|
||||
<td className="p-4 text-neutral-300 font-medium">Use Case</td>
|
||||
<td className="p-4 text-neutral-300">AI agents collaborating as peers</td>
|
||||
<td className="p-4 text-neutral-300">AI models accessing external capabilities</td>
|
||||
</tr>
|
||||
<tr className="border-b border-[#333]">
|
||||
<td className="p-4 text-neutral-300 font-medium">Relationship</td>
|
||||
<td className="p-4 text-neutral-300">Partner/delegate work</td>
|
||||
<td className="p-4 text-neutral-300">Use specific capabilities</td>
|
||||
</tr>
|
||||
<tr className="border-b border-[#333]">
|
||||
<td className="p-4 text-neutral-300 font-medium">Integration</td>
|
||||
<td className="p-4 text-neutral-300 text-emerald-400">✓ Can use MCP internally</td>
|
||||
<td className="p-4 text-neutral-300 text-blue-400">✓ Complements A2A</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="mt-4 bg-blue-500/10 border border-blue-500/20 rounded-lg p-4">
|
||||
<p className="text-blue-300 text-sm">
|
||||
<strong>Working Together:</strong> An A2A client agent might request an A2A server agent to perform a complex task.
|
||||
The server agent, in turn, might use MCP to interact with tools, APIs, or data sources necessary to fulfill the A2A task.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,796 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/documentation/components/FrontendImplementationSection.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ClipboardCopy } from "lucide-react";
|
||||
import { CodeBlock } from "@/app/documentation/components/CodeBlock";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
interface FrontendImplementationSectionProps {
|
||||
copyToClipboard: (text: string) => void;
|
||||
}
|
||||
|
||||
export function FrontendImplementationSection({ copyToClipboard }: FrontendImplementationSectionProps) {
|
||||
return (
|
||||
<Card className="bg-[#1a1a1a] border-[#333] text-white">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-emerald-400">Frontend implementation</CardTitle>
|
||||
<CardDescription className="text-neutral-400">
|
||||
Practical examples for implementation in React applications
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<Tabs defaultValue="standard">
|
||||
<TabsList className="bg-[#222] border-[#333] mb-4">
|
||||
<TabsTrigger value="standard" className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400">
|
||||
Standard HTTP
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="streaming" className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400">
|
||||
Streaming SSE
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="react-component" className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400">
|
||||
React component
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="standard">
|
||||
<div>
|
||||
<h3 className="text-emerald-400 text-lg font-medium mb-2">Implementation of message/send</h3>
|
||||
<p className="text-neutral-300 mb-4">
|
||||
Example of standard implementation in JavaScript/React:
|
||||
</p>
|
||||
|
||||
<div className="relative">
|
||||
<CodeBlock
|
||||
text={`async function sendTask(agentId, message) {
|
||||
// Generate unique IDs
|
||||
const taskId = crypto.randomUUID();
|
||||
const callId = crypto.randomUUID();
|
||||
|
||||
// Prepare request data
|
||||
const requestData = {
|
||||
jsonrpc: "2.0",
|
||||
id: callId,
|
||||
method: "message/send",
|
||||
params: {
|
||||
id: taskId,
|
||||
sessionId: "session-" + Math.random().toString(36).substring(2, 9),
|
||||
message: {
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: message
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// Indicate loading state
|
||||
setIsLoading(true);
|
||||
|
||||
// Send the request
|
||||
const response = await fetch(\`/api/v1/a2a/\${agentId}\`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': 'YOUR_API_KEY_HERE'
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(\`HTTP error: \${response.status}\`);
|
||||
}
|
||||
|
||||
// Process the response
|
||||
const data = await response.json();
|
||||
|
||||
// Check for errors
|
||||
if (data.error) {
|
||||
console.error('Error in response:', data.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract the agent response
|
||||
const task = data.result;
|
||||
|
||||
// Show response in UI
|
||||
if (task.status.message && task.status.message.parts) {
|
||||
const responseText = task.status.message.parts
|
||||
.filter(part => part.type === 'text')
|
||||
.map(part => part.text)
|
||||
.join('');
|
||||
|
||||
// Here you would update your React state
|
||||
// setResponse(responseText);
|
||||
|
||||
return responseText;
|
||||
}
|
||||
|
||||
return task;
|
||||
} catch (error) {
|
||||
console.error('Error sending task:', error);
|
||||
return null;
|
||||
} finally {
|
||||
// Finalize loading state
|
||||
setIsLoading(false);
|
||||
}
|
||||
}`}
|
||||
language="javascript"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||
onClick={() => copyToClipboard(`async function sendTask(agentId, message) {
|
||||
// Generate unique IDs
|
||||
const taskId = crypto.randomUUID();
|
||||
const callId = crypto.randomUUID();
|
||||
|
||||
// Prepare request data
|
||||
const requestData = {
|
||||
jsonrpc: "2.0",
|
||||
id: callId,
|
||||
method: "message/send",
|
||||
params: {
|
||||
id: taskId,
|
||||
sessionId: "session-" + Math.random().toString(36).substring(2, 9),
|
||||
message: {
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: message
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// Indicate loading state
|
||||
setIsLoading(true);
|
||||
|
||||
// Send the request
|
||||
const response = await fetch(\`/api/v1/a2a/\${agentId}\`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': 'YOUR_API_KEY_HERE'
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(\`HTTP error: \${response.status}\`);
|
||||
}
|
||||
|
||||
// Process the response
|
||||
const data = await response.json();
|
||||
|
||||
// Check for errors
|
||||
if (data.error) {
|
||||
console.error('Error in response:', data.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract the agent response
|
||||
const task = data.result;
|
||||
|
||||
// Show response in UI
|
||||
if (task.status.message && task.status.message.parts) {
|
||||
const responseText = task.status.message.parts
|
||||
.filter(part => part.type === 'text')
|
||||
.map(part => part.text)
|
||||
.join('');
|
||||
|
||||
// Here you would update your React state
|
||||
// setResponse(responseText);
|
||||
|
||||
return responseText;
|
||||
}
|
||||
|
||||
return task;
|
||||
} catch (error) {
|
||||
console.error('Error sending task:', error);
|
||||
return null;
|
||||
} finally {
|
||||
// Finalize loading state
|
||||
setIsLoading(false);
|
||||
}
|
||||
}`)}
|
||||
>
|
||||
<ClipboardCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="streaming">
|
||||
<div>
|
||||
<h3 className="text-emerald-400 text-lg font-medium mb-2">Implementation of message/stream (Streaming)</h3>
|
||||
<p className="text-neutral-300 mb-4">
|
||||
Example of implementation of streaming with Server-Sent Events (SSE):
|
||||
</p>
|
||||
|
||||
<div className="relative">
|
||||
<CodeBlock
|
||||
text={`async function initAgentStream(agentId, message, onUpdateCallback) {
|
||||
// Generate unique IDs
|
||||
const taskId = crypto.randomUUID();
|
||||
const callId = crypto.randomUUID();
|
||||
const sessionId = "session-" + Math.random().toString(36).substring(2, 9);
|
||||
|
||||
// Prepare request data for streaming
|
||||
const requestData = {
|
||||
jsonrpc: "2.0",
|
||||
id: callId,
|
||||
method: "message/stream",
|
||||
params: {
|
||||
id: taskId,
|
||||
sessionId: sessionId,
|
||||
message: {
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: message
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// Start initial POST request
|
||||
const response = await fetch(\`/api/v1/a2a/\${agentId}\`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': 'YOUR_API_KEY_HERE',
|
||||
'Accept': 'text/event-stream' // Important for SSE
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(\`HTTP error: \${response.status}\`);
|
||||
}
|
||||
|
||||
// Check content type of the response
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
// If the response is already SSE, use EventSource
|
||||
if (contentType?.includes('text/event-stream')) {
|
||||
// Use EventSource to process the stream
|
||||
setupEventSource(\`/api/v1/a2a/\${agentId}/stream?taskId=\${taskId}&key=YOUR_API_KEY_HERE\`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Function to configure EventSource
|
||||
function setupEventSource(url) {
|
||||
const eventSource = new EventSource(url);
|
||||
|
||||
// Handler for received messages
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
// Process data from the event
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Process the event
|
||||
if (data.result) {
|
||||
// Process status if available
|
||||
if (data.result.status) {
|
||||
const status = data.result.status;
|
||||
|
||||
// Extract text if available
|
||||
let currentText = '';
|
||||
if (status.message && status.message.parts) {
|
||||
const parts = status.message.parts.filter(part => part.type === 'text');
|
||||
if (parts.length > 0) {
|
||||
currentText = parts.map(part => part.text).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// Call callback with updates
|
||||
onUpdateCallback({
|
||||
text: currentText,
|
||||
state: status.state,
|
||||
complete: data.result.final === true
|
||||
});
|
||||
|
||||
// If it's the final event, close the connection
|
||||
if (data.result.final === true) {
|
||||
eventSource.close();
|
||||
onUpdateCallback({
|
||||
complete: true,
|
||||
state: status.state
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process artifact if available
|
||||
if (data.result.artifact) {
|
||||
const artifact = data.result.artifact;
|
||||
if (artifact.parts && artifact.parts.length > 0) {
|
||||
const parts = artifact.parts.filter(part => part.type === 'text');
|
||||
if (parts.length > 0) {
|
||||
const artifactText = parts.map(part => part.text).join('');
|
||||
|
||||
// Call callback with artifact
|
||||
onUpdateCallback({
|
||||
text: artifactText,
|
||||
state: 'artifact',
|
||||
complete: artifact.lastChunk === true
|
||||
});
|
||||
|
||||
// If it's the last chunk, close the connection
|
||||
if (artifact.lastChunk === true) {
|
||||
eventSource.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing event:', error);
|
||||
onUpdateCallback({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Handler for errors
|
||||
eventSource.onerror = (error) => {
|
||||
console.error('Error in EventSource:', error);
|
||||
eventSource.close();
|
||||
onUpdateCallback({
|
||||
error: 'Connection with server interrupted',
|
||||
complete: true,
|
||||
state: 'error'
|
||||
});
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in streaming:', error);
|
||||
// Notify error through callback
|
||||
onUpdateCallback({
|
||||
error: error.message,
|
||||
complete: true,
|
||||
state: 'error'
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}`}
|
||||
language="javascript"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||
onClick={() => copyToClipboard(`async function initAgentStream(agentId, message, onUpdateCallback) {
|
||||
// Generate unique IDs
|
||||
const taskId = crypto.randomUUID();
|
||||
const callId = crypto.randomUUID();
|
||||
const sessionId = "session-" + Math.random().toString(36).substring(2, 9);
|
||||
|
||||
// Prepare request data for streaming
|
||||
const requestData = {
|
||||
jsonrpc: "2.0",
|
||||
id: callId,
|
||||
method: "message/stream",
|
||||
params: {
|
||||
id: taskId,
|
||||
sessionId: sessionId,
|
||||
message: {
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: message
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// Start initial POST request
|
||||
const response = await fetch(\`/api/v1/a2a/\${agentId}\`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': 'YOUR_API_KEY_HERE',
|
||||
'Accept': 'text/event-stream' // Important for SSE
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(\`HTTP error: \${response.status}\`);
|
||||
}
|
||||
|
||||
// Check content type of the response
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
// If the response is already SSE, use EventSource
|
||||
if (contentType?.includes('text/event-stream')) {
|
||||
// Use EventSource to process the stream
|
||||
setupEventSource(\`/api/v1/a2a/\${agentId}/stream?taskId=\${taskId}&key=YOUR_API_KEY_HERE\`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Function to configure EventSource
|
||||
function setupEventSource(url) {
|
||||
const eventSource = new EventSource(url);
|
||||
|
||||
// Handler for received messages
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
// Process data from the event
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Process the event
|
||||
if (data.result) {
|
||||
// Process status if available
|
||||
if (data.result.status) {
|
||||
const status = data.result.status;
|
||||
|
||||
// Extract text if available
|
||||
let currentText = '';
|
||||
if (status.message && status.message.parts) {
|
||||
const parts = status.message.parts.filter(part => part.type === 'text');
|
||||
if (parts.length > 0) {
|
||||
currentText = parts.map(part => part.text).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// Call callback with updates
|
||||
onUpdateCallback({
|
||||
text: currentText,
|
||||
state: status.state,
|
||||
complete: data.result.final === true
|
||||
});
|
||||
|
||||
// If it's the final event, close the connection
|
||||
if (data.result.final === true) {
|
||||
eventSource.close();
|
||||
onUpdateCallback({
|
||||
complete: true,
|
||||
state: status.state
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process artifact if available
|
||||
if (data.result.artifact) {
|
||||
const artifact = data.result.artifact;
|
||||
if (artifact.parts && artifact.parts.length > 0) {
|
||||
const parts = artifact.parts.filter(part => part.type === 'text');
|
||||
if (parts.length > 0) {
|
||||
const artifactText = parts.map(part => part.text).join('');
|
||||
|
||||
// Call callback with artifact
|
||||
onUpdateCallback({
|
||||
text: artifactText,
|
||||
state: 'artifact',
|
||||
complete: artifact.lastChunk === true
|
||||
});
|
||||
|
||||
// If it's the last chunk, close the connection
|
||||
if (artifact.lastChunk === true) {
|
||||
eventSource.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing event:', error);
|
||||
onUpdateCallback({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Handler for errors
|
||||
eventSource.onerror = (error) => {
|
||||
console.error('Error in EventSource:', error);
|
||||
eventSource.close();
|
||||
onUpdateCallback({
|
||||
error: 'Connection with server interrupted',
|
||||
complete: true,
|
||||
state: 'error'
|
||||
});
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in streaming:', error);
|
||||
// Notify error through callback
|
||||
onUpdateCallback({
|
||||
error: error.message,
|
||||
complete: true,
|
||||
state: 'error'
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}`)}
|
||||
>
|
||||
<ClipboardCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="react-component">
|
||||
<div className="mt-4">
|
||||
<h4 className="font-medium text-emerald-400 mb-2">React component with streaming support:</h4>
|
||||
<div className="relative">
|
||||
<CodeBlock
|
||||
text={`import React, { useState, useRef } from 'react';
|
||||
|
||||
function ChatComponentA2A() {
|
||||
const [message, setMessage] = useState('');
|
||||
const [response, setResponse] = useState('');
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const [status, setStatus] = useState('');
|
||||
|
||||
// Reference to the agentId
|
||||
const agentId = 'your-agent-id';
|
||||
|
||||
// Callback for streaming updates
|
||||
const handleStreamUpdate = (update) => {
|
||||
if (update.error) {
|
||||
// Handle error
|
||||
setStatus('error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update text
|
||||
setResponse(update.text);
|
||||
|
||||
// Update status
|
||||
setStatus(update.state);
|
||||
|
||||
// If it's complete, finish streaming
|
||||
if (update.complete) {
|
||||
setIsStreaming(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!message.trim()) return;
|
||||
|
||||
// Clear previous response
|
||||
setResponse('');
|
||||
setStatus('submitted');
|
||||
|
||||
// Start streaming
|
||||
setIsStreaming(true);
|
||||
|
||||
try {
|
||||
// Start stream with the agent
|
||||
await initAgentStream(agentId, message, handleStreamUpdate);
|
||||
|
||||
// Clear message field after sending
|
||||
setMessage('');
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setStatus('error');
|
||||
setIsStreaming(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Render status indicator based on status
|
||||
const renderStatusIndicator = () => {
|
||||
switch (status) {
|
||||
case 'submitted':
|
||||
return <span className="badge badge-info">Sent</span>;
|
||||
case 'working':
|
||||
return <span className="badge badge-warning">Processing</span>;
|
||||
case 'completed':
|
||||
return <span className="badge badge-success">Completed</span>;
|
||||
case 'error':
|
||||
return <span className="badge badge-danger">Error</span>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="chat-container">
|
||||
<div className="chat-messages">
|
||||
{response && (
|
||||
<div className="message agent-message">
|
||||
<div className="message-header">
|
||||
<div className="agent-name">A2A Agent</div>
|
||||
{renderStatusIndicator()}
|
||||
</div>
|
||||
<div className="message-content">
|
||||
{response}
|
||||
{status === 'working' && !response && (
|
||||
<div className="typing-indicator">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="chat-input-form">
|
||||
<input
|
||||
type="text"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
placeholder="Type your message..."
|
||||
disabled={isStreaming}
|
||||
className="chat-input"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isStreaming || !message.trim()}
|
||||
className="send-button"
|
||||
>
|
||||
{isStreaming ? 'Processing...' : 'Send'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}`}
|
||||
language="javascript"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||
onClick={() => copyToClipboard(`import React, { useState, useRef } from 'react';
|
||||
|
||||
function ChatComponentA2A() {
|
||||
const [message, setMessage] = useState('');
|
||||
const [response, setResponse] = useState('');
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const [status, setStatus] = useState('');
|
||||
|
||||
// Reference to the agentId
|
||||
const agentId = 'your-agent-id';
|
||||
|
||||
// Callback for streaming updates
|
||||
const handleStreamUpdate = (update) => {
|
||||
if (update.error) {
|
||||
// Handle error
|
||||
setStatus('error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update text
|
||||
setResponse(update.text);
|
||||
|
||||
// Update status
|
||||
setStatus(update.state);
|
||||
|
||||
// If it's complete, finish streaming
|
||||
if (update.complete) {
|
||||
setIsStreaming(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!message.trim()) return;
|
||||
|
||||
// Clear previous response
|
||||
setResponse('');
|
||||
setStatus('submitted');
|
||||
|
||||
// Start streaming
|
||||
setIsStreaming(true);
|
||||
|
||||
try {
|
||||
// Start stream with the agent
|
||||
await initAgentStream(agentId, message, handleStreamUpdate);
|
||||
|
||||
// Clear message field after sending
|
||||
setMessage('');
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setStatus('error');
|
||||
setIsStreaming(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Render status indicator based on status
|
||||
const renderStatusIndicator = () => {
|
||||
switch (status) {
|
||||
case 'submitted':
|
||||
return <span className="badge badge-info">Sent</span>;
|
||||
case 'working':
|
||||
return <span className="badge badge-warning">Processing</span>;
|
||||
case 'completed':
|
||||
return <span className="badge badge-success">Completed</span>;
|
||||
case 'error':
|
||||
return <span className="badge badge-danger">Error</span>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="chat-container">
|
||||
<div className="chat-messages">
|
||||
{response && (
|
||||
<div className="message agent-message">
|
||||
<div className="message-header">
|
||||
<div className="agent-name">A2A Agent</div>
|
||||
{renderStatusIndicator()}
|
||||
</div>
|
||||
<div className="message-content">
|
||||
{response}
|
||||
{status === 'working' && !response && (
|
||||
<div className="typing-indicator">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="chat-input-form">
|
||||
<input
|
||||
type="text"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
placeholder="Type your message..."
|
||||
disabled={isStreaming}
|
||||
className="chat-input"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isStreaming || !message.trim()}
|
||||
className="send-button"
|
||||
>
|
||||
{isStreaming ? 'Processing...' : 'Send'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}`)}
|
||||
>
|
||||
<ClipboardCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
523
frontend/app/documentation/components/HttpLabForm.tsx
Normal file
523
frontend/app/documentation/components/HttpLabForm.tsx
Normal file
@@ -0,0 +1,523 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/documentation/components/HttpLabForm.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Send, Paperclip, X, FileText, Image, File, RotateCcw, Trash2 } from "lucide-react";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
|
||||
interface AttachedFile {
|
||||
name: string;
|
||||
type: string;
|
||||
size: number;
|
||||
base64: string;
|
||||
}
|
||||
|
||||
interface HttpLabFormProps {
|
||||
agentUrl: string;
|
||||
setAgentUrl: (url: string) => void;
|
||||
apiKey: string;
|
||||
setApiKey: (key: string) => void;
|
||||
message: string;
|
||||
setMessage: (message: string) => void;
|
||||
sessionId: string;
|
||||
setSessionId: (id: string) => void;
|
||||
taskId: string;
|
||||
setTaskId: (id: string) => void;
|
||||
callId: string;
|
||||
setCallId: (id: string) => void;
|
||||
sendRequest: () => Promise<void>;
|
||||
isLoading: boolean;
|
||||
setFiles?: (files: AttachedFile[]) => void;
|
||||
a2aMethod: string;
|
||||
setA2aMethod: (method: string) => void;
|
||||
authMethod: string;
|
||||
setAuthMethod: (method: string) => void;
|
||||
generateNewIds: () => void;
|
||||
currentTaskId?: string | null;
|
||||
conversationHistory?: any[];
|
||||
clearHistory?: () => void;
|
||||
webhookUrl?: string;
|
||||
setWebhookUrl?: (url: string) => void;
|
||||
enableWebhooks?: boolean;
|
||||
setEnableWebhooks?: (enabled: boolean) => void;
|
||||
showDetailedErrors?: boolean;
|
||||
setShowDetailedErrors?: (show: boolean) => void;
|
||||
}
|
||||
|
||||
export function HttpLabForm({
|
||||
agentUrl,
|
||||
setAgentUrl,
|
||||
apiKey,
|
||||
setApiKey,
|
||||
message,
|
||||
setMessage,
|
||||
sessionId,
|
||||
setSessionId,
|
||||
taskId,
|
||||
setTaskId,
|
||||
callId,
|
||||
setCallId,
|
||||
sendRequest,
|
||||
isLoading,
|
||||
setFiles = () => {},
|
||||
a2aMethod,
|
||||
setA2aMethod,
|
||||
authMethod,
|
||||
setAuthMethod,
|
||||
generateNewIds,
|
||||
currentTaskId,
|
||||
conversationHistory,
|
||||
clearHistory,
|
||||
webhookUrl,
|
||||
setWebhookUrl,
|
||||
enableWebhooks,
|
||||
setEnableWebhooks,
|
||||
showDetailedErrors,
|
||||
setShowDetailedErrors
|
||||
}: HttpLabFormProps) {
|
||||
const [attachedFiles, setAttachedFiles] = useState<AttachedFile[]>([]);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const clearAttachedFiles = () => {
|
||||
setAttachedFiles([]);
|
||||
};
|
||||
|
||||
const handleSendRequest = async () => {
|
||||
await sendRequest();
|
||||
clearAttachedFiles();
|
||||
};
|
||||
|
||||
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.files || e.target.files.length === 0) return;
|
||||
|
||||
const maxFileSize = 5 * 1024 * 1024; // 5MB limit
|
||||
const newFiles = Array.from(e.target.files);
|
||||
|
||||
if (attachedFiles.length + newFiles.length > 5) {
|
||||
toast({
|
||||
title: "File limit exceeded",
|
||||
description: "You can only attach up to 5 files.",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const filesToAdd: AttachedFile[] = [];
|
||||
|
||||
for (const file of newFiles) {
|
||||
if (file.size > maxFileSize) {
|
||||
toast({
|
||||
title: "File too large",
|
||||
description: `The file ${file.name} exceeds the 5MB size limit.`,
|
||||
variant: "destructive"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const base64 = await readFileAsBase64(file);
|
||||
filesToAdd.push({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
base64: base64
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to read file:", error);
|
||||
toast({
|
||||
title: "Failed to read file",
|
||||
description: `Could not process ${file.name}.`,
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (filesToAdd.length > 0) {
|
||||
const updatedFiles = [...attachedFiles, ...filesToAdd];
|
||||
setAttachedFiles(updatedFiles);
|
||||
setFiles(updatedFiles);
|
||||
}
|
||||
|
||||
// Reset file input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const readFileAsBase64 = (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const result = reader.result as string;
|
||||
const base64 = result.split(',')[1]; // Remove data URL prefix
|
||||
resolve(base64);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
};
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
const updatedFiles = attachedFiles.filter((_, i) => i !== index);
|
||||
setAttachedFiles(updatedFiles);
|
||||
setFiles(updatedFiles);
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
};
|
||||
|
||||
const isImageFile = (type: string): boolean => {
|
||||
return type.startsWith('image/');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* A2A Method and Authentication Settings */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-[#1a1a1a] border border-[#333] rounded-md">
|
||||
<div>
|
||||
<label className="text-sm text-neutral-400 mb-2 block">A2A Method</label>
|
||||
<Select value={a2aMethod} onValueChange={setA2aMethod}>
|
||||
<SelectTrigger className="bg-[#222] border-[#444] text-white">
|
||||
<SelectValue placeholder="Select A2A method" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#222] border-[#444]">
|
||||
<SelectItem value="message/send" className="text-white hover:bg-[#333]">
|
||||
message/send
|
||||
</SelectItem>
|
||||
<SelectItem value="message/stream" className="text-white hover:bg-[#333]">
|
||||
message/stream
|
||||
</SelectItem>
|
||||
<SelectItem value="tasks/get" className="text-white hover:bg-[#333]">
|
||||
tasks/get
|
||||
</SelectItem>
|
||||
<SelectItem value="tasks/cancel" className="text-white hover:bg-[#333]">
|
||||
tasks/cancel
|
||||
</SelectItem>
|
||||
<SelectItem value="tasks/pushNotificationConfig/set" className="text-white hover:bg-[#333]">
|
||||
tasks/pushNotificationConfig/set
|
||||
</SelectItem>
|
||||
<SelectItem value="tasks/pushNotificationConfig/get" className="text-white hover:bg-[#333]">
|
||||
tasks/pushNotificationConfig/get
|
||||
</SelectItem>
|
||||
<SelectItem value="tasks/resubscribe" className="text-white hover:bg-[#333]">
|
||||
tasks/resubscribe
|
||||
</SelectItem>
|
||||
<SelectItem value="agent/authenticatedExtendedCard" className="text-white hover:bg-[#333]">
|
||||
agent/authenticatedExtendedCard
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-neutral-400 mb-2 block">Authentication Method</label>
|
||||
<Select value={authMethod} onValueChange={setAuthMethod}>
|
||||
<SelectTrigger className="bg-[#222] border-[#444] text-white">
|
||||
<SelectValue placeholder="Select auth method" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#222] border-[#444]">
|
||||
<SelectItem value="api-key" className="text-white hover:bg-[#333]">
|
||||
API Key (x-api-key header)
|
||||
</SelectItem>
|
||||
<SelectItem value="bearer" className="text-white hover:bg-[#333]">
|
||||
Bearer Token (Authorization header)
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Multi-turn Conversation History Controls */}
|
||||
{(a2aMethod === "message/send" || a2aMethod === "message/stream") && conversationHistory && conversationHistory.length > 0 && (
|
||||
<div className="p-4 bg-emerald-500/5 border border-emerald-500/20 rounded-md">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm font-medium text-emerald-400">
|
||||
Multi-turn Conversation Active
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-emerald-300">
|
||||
💬 {conversationHistory.length} messages in conversation history (contextId active)
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Push Notifications (Webhook) Configuration */}
|
||||
{(a2aMethod === "message/send" || a2aMethod === "message/stream" || a2aMethod.startsWith("tasks/")) && (
|
||||
<div className="p-4 bg-blue-500/5 border border-blue-500/20 rounded-md">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="enableWebhooks"
|
||||
checked={enableWebhooks}
|
||||
onChange={(e) => setEnableWebhooks?.(e.target.checked)}
|
||||
className="rounded bg-[#222] border-[#444] text-blue-400 focus:ring-blue-400"
|
||||
/>
|
||||
<label htmlFor="enableWebhooks" className="text-sm font-medium text-blue-400">
|
||||
Enable Push Notifications (Webhooks)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{enableWebhooks && (
|
||||
<div className="mt-3">
|
||||
<label className="text-sm text-neutral-400 mb-1 block">Webhook URL</label>
|
||||
<Input
|
||||
value={webhookUrl}
|
||||
onChange={(e) => setWebhookUrl?.(e.target.value)}
|
||||
placeholder="https://your-server.com/webhook/a2a"
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
<div className="text-xs text-blue-300 mt-1">
|
||||
{a2aMethod === "tasks/pushNotificationConfig/set"
|
||||
? "📡 Configure push notifications for task"
|
||||
: "📡 Webhook URL for push notifications (configured via pushNotificationConfig)"
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!enableWebhooks && (
|
||||
<div className="text-xs text-neutral-400">
|
||||
{a2aMethod === "tasks/pushNotificationConfig/set"
|
||||
? "Push notification configuration will be set to null."
|
||||
: "No push notifications will be configured for this request."
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Advanced Error Handling Configuration */}
|
||||
<div className="p-4 bg-orange-500/5 border border-orange-500/20 rounded-md">
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="showDetailedErrors"
|
||||
checked={showDetailedErrors}
|
||||
onChange={(e) => setShowDetailedErrors?.(e.target.checked)}
|
||||
className="rounded bg-[#222] border-[#444] text-orange-400 focus:ring-orange-400"
|
||||
/>
|
||||
<label htmlFor="showDetailedErrors" className="text-sm font-medium text-orange-400">
|
||||
Enable Detailed Error Logging
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-neutral-400">
|
||||
{showDetailedErrors
|
||||
? "🔍 Detailed error information will be shown in debug logs (client-side only)."
|
||||
: "⚡ Basic error handling only - minimal error information in logs."
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-neutral-400 mb-1 block">Agent URL</label>
|
||||
<Input
|
||||
value={agentUrl}
|
||||
onChange={(e) => setAgentUrl(e.target.value)}
|
||||
placeholder="http://localhost:8000/api/v1/a2a/your-agent-id"
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-neutral-400 mb-1 block">
|
||||
{authMethod === "bearer" ? "Bearer Token" : "API Key"} (optional)
|
||||
</label>
|
||||
<Input
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder={authMethod === "bearer" ? "Your Bearer token" : "Your API key"}
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Show current task ID if available */}
|
||||
{currentTaskId && (
|
||||
<div className="p-3 bg-[#1a1a1a] border border-emerald-400/20 rounded-md">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-sm text-neutral-400">Current Task ID:</span>
|
||||
<span className="ml-2 text-emerald-400 font-mono text-sm">{currentTaskId}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Message input - only show for message methods */}
|
||||
{(a2aMethod === "message/send" || a2aMethod === "message/stream") && (
|
||||
<div>
|
||||
<label className="text-sm text-neutral-400 mb-1 block">Message</label>
|
||||
<Textarea
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
placeholder="What is the A2A protocol?"
|
||||
className="bg-[#222] border-[#444] text-white min-h-[100px]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* File attachment - only show for message methods */}
|
||||
{(a2aMethod === "message/send" || a2aMethod === "message/stream") && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-sm text-neutral-400">
|
||||
Attach Files (up to 5, max 5MB each)
|
||||
</label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={attachedFiles.length >= 5}
|
||||
>
|
||||
<Paperclip className="h-4 w-4 mr-2" />
|
||||
Browse Files
|
||||
</Button>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
className="hidden"
|
||||
multiple
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{attachedFiles.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{attachedFiles.map((file, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-1.5 bg-[#333] text-white rounded-md p-1.5 text-xs"
|
||||
>
|
||||
{isImageFile(file.type) ? (
|
||||
<Image className="h-4 w-4 text-emerald-400" />
|
||||
) : file.type === 'application/pdf' ? (
|
||||
<FileText className="h-4 w-4 text-emerald-400" />
|
||||
) : (
|
||||
<File className="h-4 w-4 text-emerald-400" />
|
||||
)}
|
||||
<span className="max-w-[150px] truncate">{file.name}</span>
|
||||
<span className="text-neutral-400">({formatFileSize(file.size)})</span>
|
||||
<button
|
||||
onClick={() => removeFile(index)}
|
||||
className="ml-1 text-neutral-400 hover:text-white transition-colors"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Separator className="my-4 bg-[#333]" />
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<label className="text-sm text-neutral-400">Session ID</label>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={generateNewIds}
|
||||
className="h-6 px-2 text-xs text-neutral-400 hover:text-white"
|
||||
>
|
||||
<RotateCcw className="h-3 w-3 mr-1" />
|
||||
New IDs
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
value={sessionId}
|
||||
onChange={(e) => setSessionId(e.target.value)}
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-neutral-400 mb-1 block">
|
||||
{a2aMethod.startsWith("tasks/") ? "Task ID (for operation)" : "Message ID (UUID)"}
|
||||
</label>
|
||||
<Input
|
||||
value={taskId}
|
||||
onChange={(e) => setTaskId(e.target.value)}
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
placeholder={a2aMethod.startsWith("tasks/") ? "Task ID to query/cancel" : "UUID for message"}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-neutral-400 mb-1 block">Request ID</label>
|
||||
<Input
|
||||
value={callId}
|
||||
onChange={(e) => setCallId(e.target.value)}
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
placeholder="req-123"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleSendRequest}
|
||||
disabled={isLoading}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d] w-full mt-4"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-black mr-2"></div>
|
||||
Sending...
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
{a2aMethod === "message/send" && "Send Message"}
|
||||
{a2aMethod === "message/stream" && "Start Stream"}
|
||||
{a2aMethod === "tasks/get" && "Get Task Status"}
|
||||
{a2aMethod === "tasks/cancel" && "Cancel Task"}
|
||||
{a2aMethod === "tasks/pushNotificationConfig/set" && "Set Push Config"}
|
||||
{a2aMethod === "tasks/pushNotificationConfig/get" && "Get Push Config"}
|
||||
{a2aMethod === "tasks/resubscribe" && "Resubscribe to Task"}
|
||||
{a2aMethod === "agent/authenticatedExtendedCard" && "Get Agent Card"}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
201
frontend/app/documentation/components/LabSection.tsx
Normal file
201
frontend/app/documentation/components/LabSection.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/documentation/components/LabSection.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ClipboardCopy } from "lucide-react";
|
||||
import { HttpLabForm } from "@/app/documentation/components/HttpLabForm";
|
||||
import { StreamLabForm } from "@/app/documentation/components/StreamLabForm";
|
||||
import { CodeBlock } from "@/app/documentation/components/CodeBlock";
|
||||
|
||||
interface LabSectionProps {
|
||||
agentUrl: string;
|
||||
setAgentUrl: (url: string) => void;
|
||||
apiKey: string;
|
||||
setApiKey: (key: string) => void;
|
||||
message: string;
|
||||
setMessage: (message: string) => void;
|
||||
sessionId: string;
|
||||
setSessionId: (id: string) => void;
|
||||
taskId: string;
|
||||
setTaskId: (id: string) => void;
|
||||
callId: string;
|
||||
setCallId: (id: string) => void;
|
||||
a2aMethod: string;
|
||||
setA2aMethod: (method: string) => void;
|
||||
authMethod: string;
|
||||
setAuthMethod: (method: string) => void;
|
||||
generateNewIds: () => void;
|
||||
sendRequest: () => Promise<void>;
|
||||
sendStreamRequestWithEventSource: () => Promise<void>;
|
||||
isLoading: boolean;
|
||||
isStreaming: boolean;
|
||||
streamResponse: string;
|
||||
streamStatus: string;
|
||||
streamHistory: string[];
|
||||
streamComplete: boolean;
|
||||
response: string;
|
||||
copyToClipboard: (text: string) => void;
|
||||
renderStatusIndicator: () => JSX.Element | null;
|
||||
renderTypingIndicator: () => JSX.Element | null;
|
||||
}
|
||||
|
||||
export function LabSection({
|
||||
agentUrl,
|
||||
setAgentUrl,
|
||||
apiKey,
|
||||
setApiKey,
|
||||
message,
|
||||
setMessage,
|
||||
sessionId,
|
||||
setSessionId,
|
||||
taskId,
|
||||
setTaskId,
|
||||
callId,
|
||||
setCallId,
|
||||
a2aMethod,
|
||||
setA2aMethod,
|
||||
authMethod,
|
||||
setAuthMethod,
|
||||
generateNewIds,
|
||||
sendRequest,
|
||||
sendStreamRequestWithEventSource,
|
||||
isLoading,
|
||||
isStreaming,
|
||||
streamResponse,
|
||||
streamStatus,
|
||||
streamHistory,
|
||||
streamComplete,
|
||||
response,
|
||||
copyToClipboard,
|
||||
renderStatusIndicator,
|
||||
renderTypingIndicator
|
||||
}: LabSectionProps) {
|
||||
const [labMode, setLabMode] = useState("http");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="bg-[#1a1a1a] border-[#333] text-white mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-emerald-400">A2A Test Lab</CardTitle>
|
||||
<CardDescription className="text-neutral-400">
|
||||
Test your A2A agent with different communication methods
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="http" onValueChange={setLabMode}>
|
||||
<TabsList className="bg-[#222] border-[#333] mb-4">
|
||||
<TabsTrigger value="http" className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400">
|
||||
HTTP Request
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="stream" className="data-[state=active]:bg-[#333] data-[state=active]:text-emerald-400">
|
||||
Streaming
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="http">
|
||||
<HttpLabForm
|
||||
agentUrl={agentUrl}
|
||||
setAgentUrl={setAgentUrl}
|
||||
apiKey={apiKey}
|
||||
setApiKey={setApiKey}
|
||||
message={message}
|
||||
setMessage={setMessage}
|
||||
sessionId={sessionId}
|
||||
setSessionId={setSessionId}
|
||||
taskId={taskId}
|
||||
setTaskId={setTaskId}
|
||||
callId={callId}
|
||||
setCallId={setCallId}
|
||||
a2aMethod={a2aMethod}
|
||||
setA2aMethod={setA2aMethod}
|
||||
authMethod={authMethod}
|
||||
setAuthMethod={setAuthMethod}
|
||||
generateNewIds={generateNewIds}
|
||||
sendRequest={sendRequest}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="stream">
|
||||
<StreamLabForm
|
||||
agentUrl={agentUrl}
|
||||
setAgentUrl={setAgentUrl}
|
||||
apiKey={apiKey}
|
||||
setApiKey={setApiKey}
|
||||
message={message}
|
||||
setMessage={setMessage}
|
||||
sessionId={sessionId}
|
||||
setSessionId={setSessionId}
|
||||
taskId={taskId}
|
||||
setTaskId={setTaskId}
|
||||
callId={callId}
|
||||
setCallId={setCallId}
|
||||
authMethod={authMethod}
|
||||
sendStreamRequest={sendStreamRequestWithEventSource}
|
||||
isStreaming={isStreaming}
|
||||
streamResponse={streamResponse}
|
||||
streamStatus={streamStatus}
|
||||
streamHistory={streamHistory}
|
||||
renderStatusIndicator={renderStatusIndicator}
|
||||
renderTypingIndicator={renderTypingIndicator}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{response && labMode === "http" && (
|
||||
<Card className="bg-[#1a1a1a] border-[#333] text-white">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-emerald-400">Response</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="relative">
|
||||
<CodeBlock
|
||||
text={response}
|
||||
language="json"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||
onClick={() => copyToClipboard(response)}
|
||||
>
|
||||
<ClipboardCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
179
frontend/app/documentation/components/QuickStartTemplates.tsx
Normal file
179
frontend/app/documentation/components/QuickStartTemplates.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/documentation/components/QuickStartTemplates.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
MessageSquare,
|
||||
FileText,
|
||||
Zap,
|
||||
Settings,
|
||||
Users,
|
||||
Play
|
||||
} from "lucide-react";
|
||||
|
||||
interface QuickStartTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: any;
|
||||
method: string;
|
||||
message: string;
|
||||
useCase: string;
|
||||
}
|
||||
|
||||
interface QuickStartTemplatesProps {
|
||||
onSelectTemplate: (template: QuickStartTemplate) => void;
|
||||
}
|
||||
|
||||
export function QuickStartTemplates({ onSelectTemplate }: QuickStartTemplatesProps) {
|
||||
const templates: QuickStartTemplate[] = [
|
||||
{
|
||||
id: "hello",
|
||||
name: "Hello Agent",
|
||||
description: "Simple greeting to test agent connectivity",
|
||||
icon: MessageSquare,
|
||||
method: "message/send",
|
||||
message: "Hello! Can you introduce yourself and tell me what you can do?",
|
||||
useCase: "Basic connectivity test"
|
||||
},
|
||||
{
|
||||
id: "analysis",
|
||||
name: "Data Analysis",
|
||||
description: "Request data analysis and insights",
|
||||
icon: FileText,
|
||||
method: "message/send",
|
||||
message: "Please analyze the current market trends in AI technology and provide key insights with recommendations.",
|
||||
useCase: "Complex analytical tasks"
|
||||
},
|
||||
{
|
||||
id: "streaming",
|
||||
name: "Long Content",
|
||||
description: "Generate lengthy content with streaming",
|
||||
icon: Zap,
|
||||
method: "message/stream",
|
||||
message: "Write a comprehensive guide about implementing the Agent2Agent protocol, including technical details, best practices, and code examples.",
|
||||
useCase: "Streaming responses"
|
||||
},
|
||||
{
|
||||
id: "task-query",
|
||||
name: "Task Status",
|
||||
description: "Query the status of a running task",
|
||||
icon: Settings,
|
||||
method: "tasks/get",
|
||||
message: "",
|
||||
useCase: "Task management"
|
||||
},
|
||||
{
|
||||
id: "capabilities",
|
||||
name: "Agent Capabilities",
|
||||
description: "Discover agent capabilities and skills",
|
||||
icon: Users,
|
||||
method: "agent/authenticatedExtendedCard",
|
||||
message: "",
|
||||
useCase: "Agent discovery"
|
||||
}
|
||||
];
|
||||
|
||||
const getMethodColor = (method: string) => {
|
||||
switch (method) {
|
||||
case 'message/send': return 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30';
|
||||
case 'message/stream': return 'bg-blue-500/20 text-blue-400 border-blue-500/30';
|
||||
case 'tasks/get': return 'bg-purple-500/20 text-purple-400 border-purple-500/30';
|
||||
case 'tasks/cancel': return 'bg-red-500/20 text-red-400 border-red-500/30';
|
||||
case 'agent/authenticatedExtendedCard': return 'bg-orange-500/20 text-orange-400 border-orange-500/30';
|
||||
default: return 'bg-neutral-500/20 text-neutral-400 border-neutral-500/30';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-[#1a1a1a] border-[#333] text-white mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-emerald-400 flex items-center">
|
||||
<Play className="h-5 w-5 mr-2" />
|
||||
Quick Start Templates
|
||||
</CardTitle>
|
||||
<p className="text-neutral-400 text-sm">
|
||||
Choose a template to quickly test different A2A protocol methods
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{templates.map((template) => {
|
||||
const IconComponent = template.icon;
|
||||
return (
|
||||
<div
|
||||
key={template.id}
|
||||
className="bg-[#222] border border-[#444] rounded-lg p-4 hover:border-emerald-500/50 transition-colors cursor-pointer group"
|
||||
onClick={() => onSelectTemplate(template)}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="bg-emerald-500/20 p-2 rounded-lg">
|
||||
<IconComponent className="h-4 w-4 text-emerald-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-white text-sm">{template.name}</h3>
|
||||
<p className="text-xs text-neutral-400">{template.useCase}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-neutral-300 mb-3 line-clamp-2">
|
||||
{template.description}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge className={`text-xs ${getMethodColor(template.method)}`}>
|
||||
{template.method}
|
||||
</Badge>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/10 text-xs px-2 py-1 h-auto opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
Use Template
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg">
|
||||
<p className="text-blue-300 text-xs">
|
||||
💡 <strong>Tip:</strong> These templates automatically configure the correct A2A method and provide example messages.
|
||||
Simply select one and customize the agent URL and authentication.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
366
frontend/app/documentation/components/StreamLabForm.tsx
Normal file
366
frontend/app/documentation/components/StreamLabForm.tsx
Normal file
@@ -0,0 +1,366 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/documentation/components/StreamLabForm.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Send, Paperclip, X, FileText, Image, File } from "lucide-react";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
|
||||
interface AttachedFile {
|
||||
name: string;
|
||||
type: string;
|
||||
size: number;
|
||||
base64: string;
|
||||
}
|
||||
|
||||
interface StreamLabFormProps {
|
||||
agentUrl: string;
|
||||
setAgentUrl: (url: string) => void;
|
||||
apiKey: string;
|
||||
setApiKey: (key: string) => void;
|
||||
message: string;
|
||||
setMessage: (message: string) => void;
|
||||
sessionId: string;
|
||||
setSessionId: (id: string) => void;
|
||||
taskId: string;
|
||||
setTaskId: (id: string) => void;
|
||||
callId: string;
|
||||
setCallId: (id: string) => void;
|
||||
sendStreamRequest: () => Promise<void>;
|
||||
isStreaming: boolean;
|
||||
streamResponse: string;
|
||||
streamStatus: string;
|
||||
streamHistory: string[];
|
||||
renderStatusIndicator: () => JSX.Element | null;
|
||||
renderTypingIndicator: () => JSX.Element | null;
|
||||
setFiles?: (files: AttachedFile[]) => void;
|
||||
authMethod: string;
|
||||
currentTaskId?: string | null;
|
||||
}
|
||||
|
||||
export function StreamLabForm({
|
||||
agentUrl,
|
||||
setAgentUrl,
|
||||
apiKey,
|
||||
setApiKey,
|
||||
message,
|
||||
setMessage,
|
||||
sessionId,
|
||||
setSessionId,
|
||||
taskId,
|
||||
setTaskId,
|
||||
callId,
|
||||
setCallId,
|
||||
sendStreamRequest,
|
||||
isStreaming,
|
||||
streamResponse,
|
||||
streamStatus,
|
||||
streamHistory,
|
||||
renderStatusIndicator,
|
||||
renderTypingIndicator,
|
||||
setFiles = () => {},
|
||||
authMethod,
|
||||
currentTaskId
|
||||
}: StreamLabFormProps) {
|
||||
const [attachedFiles, setAttachedFiles] = useState<AttachedFile[]>([]);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const clearAttachedFiles = () => {
|
||||
setAttachedFiles([]);
|
||||
};
|
||||
|
||||
const handleSendStreamRequest = async () => {
|
||||
await sendStreamRequest();
|
||||
clearAttachedFiles();
|
||||
};
|
||||
|
||||
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.files || e.target.files.length === 0) return;
|
||||
|
||||
const maxFileSize = 5 * 1024 * 1024; // 5MB limit
|
||||
const newFiles = Array.from(e.target.files);
|
||||
|
||||
if (attachedFiles.length + newFiles.length > 5) {
|
||||
toast({
|
||||
title: "File limit exceeded",
|
||||
description: "You can only attach up to 5 files.",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const filesToAdd: AttachedFile[] = [];
|
||||
|
||||
for (const file of newFiles) {
|
||||
if (file.size > maxFileSize) {
|
||||
toast({
|
||||
title: "File too large",
|
||||
description: `The file ${file.name} exceeds the 5MB size limit.`,
|
||||
variant: "destructive"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const base64 = await readFileAsBase64(file);
|
||||
filesToAdd.push({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
base64: base64
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to read file:", error);
|
||||
toast({
|
||||
title: "Failed to read file",
|
||||
description: `Could not process ${file.name}.`,
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (filesToAdd.length > 0) {
|
||||
const updatedFiles = [...attachedFiles, ...filesToAdd];
|
||||
setAttachedFiles(updatedFiles);
|
||||
setFiles(updatedFiles);
|
||||
}
|
||||
|
||||
// Reset file input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const readFileAsBase64 = (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const result = reader.result as string;
|
||||
const base64 = result.split(',')[1]; // Remove data URL prefix
|
||||
resolve(base64);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
};
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
const updatedFiles = attachedFiles.filter((_, i) => i !== index);
|
||||
setAttachedFiles(updatedFiles);
|
||||
setFiles(updatedFiles);
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
};
|
||||
|
||||
const isImageFile = (type: string): boolean => {
|
||||
return type.startsWith('image/');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* A2A Streaming Information */}
|
||||
<div className="p-4 bg-[#1a1a1a] border border-[#333] rounded-md">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-emerald-400">A2A Streaming Mode</span>
|
||||
<span className="text-xs text-neutral-400">Method: message/stream</span>
|
||||
</div>
|
||||
<div className="text-xs text-neutral-400">
|
||||
Authentication: {authMethod === "bearer" ? "Bearer Token" : "API Key"} header
|
||||
</div>
|
||||
{currentTaskId && (
|
||||
<div className="mt-2 pt-2 border-t border-[#333]">
|
||||
<span className="text-xs text-neutral-400">Current Task ID: </span>
|
||||
<span className="text-xs text-emerald-400 font-mono">{currentTaskId}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-neutral-400 mb-1 block">Agent URL</label>
|
||||
<Input
|
||||
value={agentUrl}
|
||||
onChange={(e) => setAgentUrl(e.target.value)}
|
||||
placeholder="http://localhost:8000/api/v1/a2a/your-agent-id"
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
disabled={isStreaming}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-neutral-400 mb-1 block">
|
||||
{authMethod === "bearer" ? "Bearer Token" : "API Key"} (optional)
|
||||
</label>
|
||||
<Input
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder={authMethod === "bearer" ? "Your Bearer token" : "Your API key"}
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
disabled={isStreaming}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm text-neutral-400 mb-1 block">Message</label>
|
||||
<Textarea
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
placeholder="What is the A2A protocol?"
|
||||
className="bg-[#222] border-[#444] text-white min-h-[100px]"
|
||||
disabled={isStreaming}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-sm text-neutral-400">
|
||||
Attach Files (up to 5, max 5MB each)
|
||||
</label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-[#222] border-[#444] text-neutral-300 hover:bg-[#333] hover:text-white"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={attachedFiles.length >= 5 || isStreaming}
|
||||
>
|
||||
<Paperclip className="h-4 w-4 mr-2" />
|
||||
Browse Files
|
||||
</Button>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
className="hidden"
|
||||
multiple
|
||||
onChange={handleFileSelect}
|
||||
disabled={isStreaming}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{attachedFiles.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{attachedFiles.map((file, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-1.5 bg-[#333] text-white rounded-md p-1.5 text-xs"
|
||||
>
|
||||
{isImageFile(file.type) ? (
|
||||
<Image className="h-4 w-4 text-emerald-400" />
|
||||
) : file.type === 'application/pdf' ? (
|
||||
<FileText className="h-4 w-4 text-emerald-400" />
|
||||
) : (
|
||||
<File className="h-4 w-4 text-emerald-400" />
|
||||
)}
|
||||
<span className="max-w-[150px] truncate">{file.name}</span>
|
||||
<span className="text-neutral-400">({formatFileSize(file.size)})</span>
|
||||
<button
|
||||
onClick={() => removeFile(index)}
|
||||
className="ml-1 text-neutral-400 hover:text-white transition-colors"
|
||||
disabled={isStreaming}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator className="my-4 bg-[#333]" />
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-neutral-400 mb-1 block">Session ID</label>
|
||||
<Input
|
||||
value={sessionId}
|
||||
onChange={(e) => setSessionId(e.target.value)}
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
disabled={isStreaming}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-neutral-400 mb-1 block">Task ID</label>
|
||||
<Input
|
||||
value={taskId}
|
||||
onChange={(e) => setTaskId(e.target.value)}
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
disabled={isStreaming}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-neutral-400 mb-1 block">Call ID</label>
|
||||
<Input
|
||||
value={callId}
|
||||
onChange={(e) => setCallId(e.target.value)}
|
||||
className="bg-[#222] border-[#444] text-white"
|
||||
disabled={isStreaming}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleSendStreamRequest}
|
||||
disabled={isStreaming}
|
||||
className="bg-emerald-400 text-black hover:bg-[#00cc7d] w-full mt-4"
|
||||
>
|
||||
{isStreaming ? (
|
||||
<div className="flex items-center">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-black mr-2"></div>
|
||||
Streaming...
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
Start Streaming
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{streamResponse && (
|
||||
<div className="mt-6 rounded-md bg-[#222] border border-[#333] p-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h3 className="text-lg font-medium text-white">Response</h3>
|
||||
{renderStatusIndicator && renderStatusIndicator()}
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap text-sm font-mono text-neutral-300">
|
||||
{streamResponse}
|
||||
</div>
|
||||
{renderTypingIndicator && renderTypingIndicator()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/documentation/components/TechnicalDetailsSection.tsx │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ClipboardCopy } from "lucide-react";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { CodeBlock } from "@/app/documentation/components/CodeBlock";
|
||||
|
||||
interface TechnicalDetailsSectionProps {
|
||||
copyToClipboard: (text: string) => void;
|
||||
}
|
||||
|
||||
export function TechnicalDetailsSection({ copyToClipboard }: TechnicalDetailsSectionProps) {
|
||||
return (
|
||||
<Card className="bg-[#1a1a1a] border-[#333] text-white">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-emerald-400">Technical Details of the Methods</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-emerald-400 text-lg font-medium mb-2">Method message/send</h3>
|
||||
<p className="text-neutral-300 mb-4">
|
||||
The <code className="bg-[#333] px-1 rounded">message/send</code> method performs a standard HTTP request and waits for the complete response.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium text-white mb-2">Request:</h4>
|
||||
<div className="relative">
|
||||
<CodeBlock
|
||||
text={JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: "call-123",
|
||||
method: "message/send",
|
||||
params: {
|
||||
id: "task-456",
|
||||
sessionId: "session-789",
|
||||
message: {
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Your question here"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}, null, 2)}
|
||||
language="json"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||
onClick={() => copyToClipboard(JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: "call-123",
|
||||
method: "message/send",
|
||||
params: {
|
||||
id: "task-456",
|
||||
sessionId: "session-789",
|
||||
message: {
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Your question here"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}, null, 2))}
|
||||
>
|
||||
<ClipboardCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-white mb-2">Headers:</h4>
|
||||
<div className="relative">
|
||||
<CodeBlock
|
||||
text={`Content-Type: application/json
|
||||
x-api-key: YOUR_API_KEY`}
|
||||
language="text"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||
onClick={() => copyToClipboard(`Content-Type: application/json
|
||||
x-api-key: YOUR_API_KEY`)}
|
||||
>
|
||||
<ClipboardCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-white mb-2">Response:</h4>
|
||||
<div className="relative">
|
||||
<CodeBlock
|
||||
text={JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
result: {
|
||||
status: {
|
||||
state: "completed",
|
||||
message: {
|
||||
role: "model",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Complete agent response here."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
id: "call-123"
|
||||
}, null, 2)}
|
||||
language="json"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||
onClick={() => copyToClipboard(JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
result: {
|
||||
status: {
|
||||
state: "completed",
|
||||
message: {
|
||||
role: "model",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Complete agent response here."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
id: "call-123"
|
||||
}, null, 2))}
|
||||
>
|
||||
<ClipboardCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="my-6 bg-[#333]" />
|
||||
|
||||
<div>
|
||||
<h3 className="text-emerald-400 text-lg font-medium mb-2">Method message/stream</h3>
|
||||
<p className="text-neutral-300 mb-4">
|
||||
The <code className="bg-[#333] px-1 rounded">message/stream</code> method uses Server-Sent Events (SSE) to receive real-time updates.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium text-white mb-2">Request:</h4>
|
||||
<div className="relative">
|
||||
<CodeBlock
|
||||
text={JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: "call-123",
|
||||
method: "message/stream",
|
||||
params: {
|
||||
id: "task-456",
|
||||
sessionId: "session-789",
|
||||
message: {
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Your question here"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}, null, 2)}
|
||||
language="json"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||
onClick={() => copyToClipboard(JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: "call-123",
|
||||
method: "message/stream",
|
||||
params: {
|
||||
id: "task-456",
|
||||
sessionId: "session-789",
|
||||
message: {
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Your question here"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}, null, 2))}
|
||||
>
|
||||
<ClipboardCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-white mb-2">Headers:</h4>
|
||||
<div className="relative">
|
||||
<CodeBlock
|
||||
text={`Content-Type: application/json
|
||||
x-api-key: YOUR_API_KEY
|
||||
Accept: text/event-stream`}
|
||||
language="text"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||
onClick={() => copyToClipboard(`Content-Type: application/json
|
||||
x-api-key: YOUR_API_KEY
|
||||
Accept: text/event-stream`)}
|
||||
>
|
||||
<ClipboardCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-white mb-2">SSE Event Format:</h4>
|
||||
<p className="text-neutral-300 mb-4">
|
||||
Each event follows the standard Server-Sent Events (SSE) format, with the "data:" prefix followed by the JSON content and terminated by two newlines ("\n\n"):
|
||||
</p>
|
||||
<div className="relative">
|
||||
<CodeBlock
|
||||
text={`data: {"jsonrpc":"2.0","id":"call-123","result":{"id":"task-456","status":{"state":"working","message":{"role":"agent","parts":[{"type":"text","text":"Processing..."}]},"timestamp":"2025-05-13T18:10:37.219Z"},"final":false}}
|
||||
|
||||
data: {"jsonrpc":"2.0","id":"call-123","result":{"id":"task-456","status":{"state":"completed","timestamp":"2025-05-13T18:10:40.456Z"},"final":true}}
|
||||
`}
|
||||
language="text"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||
onClick={() => copyToClipboard(`data: {"jsonrpc":"2.0","id":"call-123","result":{"id":"task-456","status":{"state":"working","message":{"role":"agent","parts":[{"type":"text","text":"Processing..."}]},"timestamp":"2025-05-13T18:10:37.219Z"},"final":false}}
|
||||
|
||||
data: {"jsonrpc":"2.0","id":"call-123","result":{"id":"task-456","status":{"state":"completed","timestamp":"2025-05-13T18:10:40.456Z"},"final":true}}
|
||||
`)}
|
||||
>
|
||||
<ClipboardCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-white mb-2">Event Types:</h4>
|
||||
<ul className="list-disc list-inside text-neutral-300 space-y-2 mb-4">
|
||||
<li><span className="text-emerald-400">Status Updates</span>: Contains the <code className="bg-[#333] px-1 rounded">status</code> field with information about the task status.</li>
|
||||
<li><span className="text-emerald-400">Artifact Updates</span>: Contains the <code className="bg-[#333] px-1 rounded">artifact</code> field with the content generated by the agent.</li>
|
||||
<li><span className="text-emerald-400">Ping Events</span>: Simple events with the format <code className="bg-[#333] px-1 rounded">: ping</code> to keep the connection active.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-white mb-2">Client Consumption:</h4>
|
||||
<p className="text-neutral-300 mb-2">
|
||||
For a better experience, we recommend using the <code className="bg-[#333] px-1 rounded">EventSource</code> API to consume the events:
|
||||
</p>
|
||||
<div className="relative">
|
||||
<CodeBlock
|
||||
text={`// After receiving the initial response via POST, use EventSource to stream
|
||||
const eventSource = new EventSource(\`/api/v1/a2a/\${agentId}/stream?taskId=\${taskId}&key=\${apiKey}\`);
|
||||
|
||||
// Process the received events
|
||||
eventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Process different types of events
|
||||
if (data.result) {
|
||||
// 1. Process status updates
|
||||
if (data.result.status) {
|
||||
const state = data.result.status.state; // "working", "completed", etc.
|
||||
|
||||
// Check if there is a text message
|
||||
if (data.result.status.message?.parts) {
|
||||
const textParts = data.result.status.message.parts
|
||||
.filter(part => part.type === "text")
|
||||
.map(part => part.text)
|
||||
.join("");
|
||||
|
||||
// Update UI with the text
|
||||
updateUI(textParts);
|
||||
}
|
||||
|
||||
// Check if it is the final event
|
||||
if (data.result.final === true) {
|
||||
eventSource.close(); // Close connection
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Process the generated artifacts
|
||||
if (data.result.artifact) {
|
||||
const artifact = data.result.artifact;
|
||||
|
||||
// Extract text from the artifact
|
||||
if (artifact.parts) {
|
||||
const artifactText = artifact.parts
|
||||
.filter(part => part.type === "text")
|
||||
.map(part => part.text)
|
||||
.join("");
|
||||
|
||||
// Update UI with the artifact
|
||||
updateArtifactUI(artifactText);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle errors
|
||||
eventSource.onerror = (error) => {
|
||||
console.error("Error in SSE:", error);
|
||||
eventSource.close();
|
||||
};`}
|
||||
language="javascript"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2 text-white hover:bg-[#333]"
|
||||
onClick={() => copyToClipboard(`// After receiving the initial response via POST, use EventSource to stream
|
||||
const eventSource = new EventSource(\`/api/v1/a2a/\${agentId}/stream?taskId=\${taskId}&key=\${apiKey}\`);
|
||||
|
||||
// Process the received events
|
||||
eventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Process different types of events
|
||||
if (data.result) {
|
||||
// 1. Process status updates
|
||||
if (data.result.status) {
|
||||
const state = data.result.status.state; // "working", "completed", etc.
|
||||
|
||||
// Check if there is a text message
|
||||
if (data.result.status.message?.parts) {
|
||||
const textParts = data.result.status.message.parts
|
||||
.filter(part => part.type === "text")
|
||||
.map(part => part.text)
|
||||
.join("");
|
||||
|
||||
// Update UI with the text
|
||||
updateUI(textParts);
|
||||
}
|
||||
|
||||
// Check if it is the final event
|
||||
if (data.result.final === true) {
|
||||
eventSource.close(); // Close connection
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Process the generated artifacts
|
||||
if (data.result.artifact) {
|
||||
const artifact = data.result.artifact;
|
||||
|
||||
// Extract text from the artifact
|
||||
if (artifact.parts) {
|
||||
const artifactText = artifact.parts
|
||||
.filter(part => part.type === "text")
|
||||
.map(part => part.text)
|
||||
.join("");
|
||||
|
||||
// Update UI with the artifact
|
||||
updateArtifactUI(artifactText);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle errors
|
||||
eventSource.onerror = (error) => {
|
||||
console.error("Error in SSE:", error);
|
||||
eventSource.close();
|
||||
};`)}
|
||||
>
|
||||
<ClipboardCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-white mb-2">Possible task states:</h4>
|
||||
<ul className="list-disc list-inside text-neutral-300 space-y-1">
|
||||
<li><span className="text-emerald-400">submitted</span>: Task sent but not yet processed</li>
|
||||
<li><span className="text-emerald-400">working</span>: Task being processed by the agent</li>
|
||||
<li><span className="text-emerald-400">completed</span>: Task completed successfully</li>
|
||||
<li><span className="text-emerald-400">input-required</span>: Agent waiting for additional user input</li>
|
||||
<li><span className="text-emerald-400">failed</span>: Task failed during processing</li>
|
||||
<li><span className="text-emerald-400">canceled</span>: Task was canceled</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#222] p-4 rounded-md border border-[#444]">
|
||||
<h4 className="font-medium text-white mb-2">Possible task states:</h4>
|
||||
<ul className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<li className="flex items-center">
|
||||
<span className="w-3 h-3 bg-blue-500 rounded-full mr-2"></span>
|
||||
<span className="text-neutral-300"><strong>submitted</strong>: Task sent</span>
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<span className="w-3 h-3 bg-yellow-500 rounded-full mr-2"></span>
|
||||
<span className="text-neutral-300"><strong>working</strong>: Task being processed</span>
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<span className="w-3 h-3 bg-purple-500 rounded-full mr-2"></span>
|
||||
<span className="text-neutral-300"><strong>input-required</strong>: Agent waiting for additional user input</span>
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<span className="w-3 h-3 bg-green-500 rounded-full mr-2"></span>
|
||||
<span className="text-neutral-300"><strong>completed</strong>: Task completed successfully</span>
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<span className="w-3 h-3 bg-neutral-500 rounded-full mr-2"></span>
|
||||
<span className="text-neutral-300"><strong>canceled</strong>: Task canceled</span>
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<span className="w-3 h-3 bg-red-500 rounded-full mr-2"></span>
|
||||
<span className="text-neutral-300"><strong>failed</strong>: Task processing failed</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
1611
frontend/app/documentation/page.tsx
Normal file
1611
frontend/app/documentation/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
87
frontend/app/globals.css
Normal file
87
frontend/app/globals.css
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||
│ @author: Davidson Gomes │
|
||||
│ @file: /app/globals.css │
|
||||
│ Developed by: Davidson Gomes │
|
||||
│ Creation date: May 13, 2025 │
|
||||
│ Contact: contato@evolution-api.com │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @copyright © Evolution API 2025. All rights reserved. │
|
||||
│ Licensed under the Apache License, Version 2.0 │
|
||||
│ │
|
||||
│ You may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
├──────────────────────────────────────────────────────────────────────────────┤
|
||||
│ @important │
|
||||
│ For any future changes to the code in this file, it is recommended to │
|
||||
│ include, together with the modification, the information of the developer │
|
||||
│ who changed it and the date of modification. │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--primary: 142.1 70.6% 45.3%;
|
||||
--primary-foreground: 355.7 100% 97.3%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 142.1 70.6% 45.3%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 142.1 70.6% 45.3%;
|
||||
--primary-foreground: 144.9 80.4% 10%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 142.4 71.8% 29.2%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user