├── .env.example
├── .gitattributes
├── .gitignore
├── .pre-commit-config.yaml
├── LICENSE
├── README.md
├── app
└── components
│ └── Todo.tsx
├── backend
├── .dockerignore
├── .gitignore
├── Dockerfile
├── README.md
├── app
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ ├── camel_agent.py
│ │ ├── deps.py
│ │ ├── main.py
│ │ └── routes
│ │ │ ├── __init__.py
│ │ │ ├── agent.py
│ │ │ ├── approval.py
│ │ │ ├── camel.py
│ │ │ ├── human.py
│ │ │ ├── mock_data.py
│ │ │ ├── rag.py
│ │ │ ├── rolePlaying.py
│ │ │ ├── settings.py
│ │ │ └── workflow.py
│ ├── core
│ │ ├── __init__.py
│ │ └── config.py
│ ├── main.py
│ ├── models
│ │ ├── __init__.py
│ │ ├── approval.py
│ │ ├── chat.py
│ │ ├── human.py
│ │ ├── rag.py
│ │ ├── settings.py
│ │ └── workflow.py
│ └── tests
│ │ ├── __init__.py
│ │ └── conftest.py
├── poetry.lock
├── pyproject.toml
├── scripts
│ ├── format.sh
│ ├── lint.sh
│ └── test.sh
└── tests-start.sh
├── docker-compose.override.yml
├── docker-compose.traefik.yml
├── docker-compose.yml
├── draft_prd.markdown
├── frontend
├── .dockerignore
├── .env
├── .gitignore
├── .nvmrc
├── Dockerfile
├── README.md
├── biome.json
├── components.json
├── eslint.config.js
├── index.html
├── modify-openapi-operationids.js
├── nginx-backend-not-found.conf
├── nginx.conf
├── package-lock.json
├── package.json
├── playwright.config.ts
├── playwright
│ └── .auth
│ │ └── user.json
├── public
│ └── assets
│ │ └── images
│ │ ├── camel-logo-purple.svg
│ │ ├── camel-logo.svg
│ │ └── favicon.png
├── src
│ ├── AIPlayground.css
│ ├── AIPlayground.tsx
│ ├── App.css
│ ├── App.tsx
│ ├── Canvas.tsx
│ ├── assets
│ │ ├── fonts
│ │ │ ├── OpenSans.ttf
│ │ │ └── Palatino.ttf
│ │ └── react.svg
│ ├── canvas.css
│ ├── components
│ │ ├── app-sidebar.tsx
│ │ ├── nav-main.tsx
│ │ ├── nav-projects.tsx
│ │ ├── nav-user.tsx
│ │ ├── team-switcher.tsx
│ │ └── ui
│ │ │ ├── avatar.tsx
│ │ │ ├── breadcrumb.tsx
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ ├── chat-input.tsx
│ │ │ ├── chat
│ │ │ ├── chat-bubble.tsx
│ │ │ ├── chat-input.tsx
│ │ │ ├── chat-message-list.tsx
│ │ │ ├── expandable-chat.tsx
│ │ │ ├── hooks
│ │ │ │ └── useAutoScroll.tsx
│ │ │ └── message-loading.tsx
│ │ │ ├── checkbox.tsx
│ │ │ ├── code-block.tsx
│ │ │ ├── code-editor.tsx
│ │ │ ├── collapsible.tsx
│ │ │ ├── command.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── dropdown-menu.tsx
│ │ │ ├── file-upload.tsx
│ │ │ ├── input.tsx
│ │ │ ├── label.tsx
│ │ │ ├── resizable.tsx
│ │ │ ├── select.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── sheet.tsx
│ │ │ ├── sidebar.tsx
│ │ │ ├── skeleton.tsx
│ │ │ ├── slider.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── tabs.tsx
│ │ │ └── tooltip.tsx
│ ├── hooks
│ │ └── use-mobile.ts
│ ├── index.css
│ ├── library
│ │ └── utils.ts
│ ├── main.tsx
│ ├── routeTree.gen.ts
│ ├── routes
│ │ ├── __root.tsx
│ │ ├── _layout.tsx
│ │ ├── _layout
│ │ │ ├── admin.tsx
│ │ │ ├── index.tsx
│ │ │ ├── settings.tsx
│ │ │ └── tasks.tsx
│ │ ├── login.tsx
│ │ └── signup.tsx
│ └── vite-env.d.ts
├── tests
│ ├── auth.setup.ts
│ ├── camel.spec.ts
│ ├── config.ts
│ ├── home.spec.ts
│ └── utils
│ │ ├── random.ts
│ │ └── user.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
├── hooks
└── post_gen_project.py
└── scripts
├── build-push.sh
├── build.sh
├── deploy.sh
├── test-local.sh
└── test.sh
/.env.example:
--------------------------------------------------------------------------------
1 | # Domain
2 | # This would be set to the production domain with an env var on deployment
3 | DOMAIN=localhost
4 | # To test the local Traefik config
5 | # DOMAIN=localhost.app.camel-ai.org
6 |
7 | # Used by the backend to generate links in emails to the frontend
8 | FRONTEND_HOST=http://localhost:5173
9 | # In staging and production, set this env var to the frontend host, e.g.
10 | # FRONTEND_HOST=http://localhost.app.camel-ai.org
11 |
12 | # INSTALL_DEV: boolean, set to true to install dev dependencies
13 | INSTALL_DEV=false
14 | # Environment: local, staging, production
15 | ENVIRONMENT=local
16 |
17 | PROJECT_NAME=CamelWebApp
18 | STACK_NAME=camelwebapp
19 |
20 | # Celery
21 | CELERY_BROKER_URL=redis://redis:6379/0
22 | CELERY_RESULT_BACKEND=redis://redis:6379/0
23 |
24 | # Backend
25 | BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,https://localhost:5173,http://localhost.app.camel-ai.org,http://localhost.app.camel-ai.org"
26 | SECRET_KEY=changethis
27 | FIRST_SUPERUSER=admin@example.com
28 | FIRST_SUPERUSER_PASSWORD=changethis
29 |
30 | # Postgres
31 | POSTGRES_SERVER=localhost
32 | POSTGRES_PORT=5432
33 | POSTGRES_DB=app
34 | POSTGRES_USER=postgres
35 | POSTGRES_PASSWORD=changethis
36 |
37 | SENTRY_DSN=
38 |
39 | # Configure these with your own Docker registry images
40 | DOCKER_IMAGE_BACKEND=camelwebapp-backend
41 | DOCKER_IMAGE_FRONTEND=camelwebapp-frontend
42 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 | *.sh text eol=lf
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
110 | .pdm.toml
111 | .pdm-python
112 | .pdm-build/
113 |
114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
115 | __pypackages__/
116 |
117 | # Celery stuff
118 | celerybeat-schedule
119 | celerybeat.pid
120 |
121 | # SageMath parsed files
122 | *.sage.py
123 |
124 | # Environments
125 | .env
126 | .venv
127 | env/
128 | venv/
129 | ENV/
130 | env.bak/
131 | venv.bak/
132 |
133 | # Spyder project settings
134 | .spyderproject
135 | .spyproject
136 |
137 | # Rope project settings
138 | .ropeproject
139 |
140 | # mkdocs documentation
141 | /site
142 |
143 | # mypy
144 | .mypy_cache/
145 | .dmypy.json
146 | dmypy.json
147 |
148 | # Pyre type checker
149 | .pyre/
150 |
151 | # pytype static type analyzer
152 | .pytype/
153 |
154 | # Cython debug symbols
155 | cython_debug/
156 |
157 | # PyCharm
158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
160 | # and can be added to the global gitignore or merged into this file. For a more nuclear
161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
162 | .idea/
163 |
164 | .env.local
165 |
166 | node_modules/
167 | /test-results/
168 | /playwright-report/
169 | /blob-report/
170 | /playwright/.cache/
171 |
172 | ### NextJS template
173 | # dependencies
174 | /node_modules
175 | /.pnp
176 | .pnp.js
177 |
178 | # testing
179 | /coverage
180 |
181 | # next.js
182 | /.next/
183 | /out/
184 |
185 | # production
186 | /build
187 |
188 | # misc
189 | .DS_Store
190 | *.pem
191 |
192 | # debug
193 | npm-debug.log*
194 | yarn-debug.log*
195 | yarn-error.log*
196 | .pnpm-debug.log*
197 |
198 | # local env files
199 | .env*.local
200 |
201 | # vercel
202 | .vercel
203 |
204 | # typescript
205 | *.tsbuildinfo
206 | next-env.d.ts
207 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | # See https://pre-commit.com for more information
2 | # See https://pre-commit.com/hooks.html for more hooks
3 | repos:
4 | - repo: https://github.com/pre-commit/pre-commit-hooks
5 | rev: v4.4.0
6 | hooks:
7 | - id: check-added-large-files
8 | - id: check-toml
9 | - id: check-yaml
10 | args:
11 | - --unsafe
12 | - id: end-of-file-fixer
13 | exclude: ^frontend/src/client/.*
14 | - id: trailing-whitespace
15 | exclude: ^frontend/src/client/.*
16 | - repo: https://github.com/charliermarsh/ruff-pre-commit
17 | rev: v0.2.2
18 | hooks:
19 | - id: ruff
20 | args:
21 | - --fix
22 | - id: ruff-format
23 |
24 | ci:
25 | autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
26 | autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CAMEL Web App
2 |
3 | ## Overview
4 | CAMEL Web App is a React-based web interface designed to demonstrate CAMEL's various modules. This interactive platform enables users to explore and interact with CAMEL's capabilities through a user-friendly interface.
5 |
6 | ## Contributing
7 | We welcome contributions! Please feel free to submit a Pull Request.
8 |
9 | ## Contributors
10 | - Front-end Lead: [xinyuguan3](https://github.com/xinyuguan3)
11 | - Back-end Lead: [koch3092](https://github.com/koch3092)
12 | - Back-end: [xzjjj](https://github.com/xzjjj)
13 | - Full Stack: [User235514](https://github.com/User235514)
14 |
15 | ## Features
16 | - **Multi-Model Support**
17 | - DeepSeek
18 | - Llama
19 | - Qwen
20 | - Support for future model integrations
21 |
22 | - **Advanced Model Management**
23 | - Flexible model switching
24 | - Customizable system messages
25 | - Configurable model parameters
26 | - Tool integration support
27 |
28 | - **Tool Integration**
29 | - Access to CAMEL toolkits
30 | - Tool-specific configuration options
31 | - Parameter input interface
32 | - Result visualization
33 |
34 | - **Role Playing Sessions**
35 | - Multi-agent interactions
36 | - Customizable agent roles
37 | - Task-specific configurations
38 | - Session management
39 |
40 | - **Workforce Module**
41 | - Coordinate multiple specialized agents
42 | - Mix of single agents and role-playing pairs
43 | - Customizable workforce configuration
44 | - Collaborative task solving
45 | - Flexible agent role assignment
46 |
47 | ## Target Users
48 | - Developers integrating CAMEL into their applications
49 | - Researchers exploring Agent capabilities
50 | - Open-source contributors
51 |
52 | ## Technology Stack and Features
53 |
54 | - ⚡ [**FastAPI**](https://fastapi.tiangolo.com) for the Python backend API.
55 | - 🧰 [SQLModel](https://sqlmodel.tiangolo.com) for the Python SQL database interactions (ORM).
56 | - 🔍 [Pydantic](https://docs.pydantic.dev), used by FastAPI, for the data validation and settings management.
57 | - 💾 [PostgreSQL](https://www.postgresql.org) as the SQL database.
58 | - 🚀 [React](https://react.dev) for the frontend.
59 | - 💃 Using TypeScript, hooks, Vite, and other parts of a modern frontend stack.
60 | - 🎨 [Chakra UI](https://chakra-ui.com) for the frontend components.
61 | - 🤖 An automatically generated frontend client.
62 | - 🧪 [Playwright](https://playwright.dev) for End-to-End testing.
63 | - 🦇 Dark mode support.
64 | - 🐋 [Docker Compose](https://www.docker.com) for development and production.
65 | - 🔑 JWT (JSON Web Token) authentication.
66 | - ✅ Tests with [Pytest](https://pytest.org).
67 | - 📞 [Traefik](https://traefik.io) as a local reverse proxy / load balancer.
68 | - 🚢 Deployment instructions using Docker Compose, including how to set up a frontend Traefik proxy to handle automatic HTTPS certificates.
69 | - 🏭 CI (continuous integration) and CD (continuous deployment) based on GitHub Actions.
70 |
71 | ## Getting Started
72 |
73 | ### Clone Repository
74 | You can clone this repository with:
75 |
76 | ```bash
77 | git clone https://github.com/camel-ai/camel_web_app.git
78 | ```
79 |
80 | ### Configure
81 |
82 | Copy the `.env.example` files to `.env`:
83 |
84 | ```bash
85 | cp .env.example .env
86 | ```
87 |
88 | Then you can then update configs in the `.env` files to customize your configurations.
89 |
90 | Before deploying it, make sure you change at least the values for:
91 |
92 | - `SECRET_KEY`
93 | - `POSTGRES_PASSWORD`
94 |
95 | You can (and should) pass these as environment variables from secrets.
96 |
97 | Also, you can set the following environment variables:
98 |
99 | - `SENTRY_DSN`: (default: "") The DSN for Sentry, if you are using it, you can set it later in .env.
100 |
101 | ### Generate Secret Keys
102 |
103 | Some environment variables in the `.env` file have a default value of `changethis`.
104 |
105 | You have to change them with a secret key, to generate secret keys you can run the following command:
106 |
107 | ```bash
108 | python -c "import secrets; print(secrets.token_urlsafe(32))"
109 | ```
110 |
111 | Copy the content and use that as password / secret key. And run that again to generate another secure key.
112 |
113 | ### Run all services using Docker Compose
114 | You can run all the services using Docker Compose with:
115 | ```bash
116 | docker compose up -d
117 | ```
118 |
119 | You will see something like:
120 | ```bash
121 | [+] Running 15/15oard] exporting to image 0.0s
122 | ✔ Service frontend Built 41.3s
123 | ✔ Service celery-worker Built 92.7s
124 | ✔ Service backend Built 1.2s
125 | ✔ Service celery-dashboard Built 0.6s
126 | ✔ Network camel_web_app_default Created 0.0s
127 | ✔ Network camel_web_app_traefik-public Created 0.0s
128 | ✔ Volume "camel_web_app_app-db-data" Created 0.0s
129 | ✔ Container camel_web_app-proxy-1 Started 0.3s
130 | ✔ Container camel_web_app-frontend-1 Started 0.3s
131 | ✔ Container camel_web_app-db-1 Started 0.3s
132 | ✔ Container camel_web_app-redis-1 Healthy 5.8s
133 | ✔ Container camel_web_app-celery-worker-1 Started 5.9s
134 | ✔ Container camel_web_app-adminer-1 Started 0.4s
135 | ✔ Container camel_web_app-backend-1 Started 6.0s
136 | ✔ Container camel_web_app-celery-dashboard-1 Started 6.1s
137 | ```
138 | ## URLs
139 |
140 | ### Development URLs
141 |
142 | Development URLs, for local development.
143 |
144 | Frontend: http://localhost:5173
145 |
146 | Backend: http://localhost:8000
147 |
148 | Automatic Interactive Docs (Swagger UI): http://localhost:8000/docs
149 |
150 | Automatic Alternative Docs (ReDoc): http://localhost:8000/redoc
151 |
152 | Adminer: http://localhost:8080
153 |
154 | Traefik UI: http://localhost:8090
155 |
156 | ### Development URLs with `localhost.app.camel-ai.org` Configured
157 |
158 | Development URLs, for local development.
159 |
160 | Frontend: http://localhost.app.camel-ai.org
161 |
162 | Backend: http://localhost.app.camel-ai.org/api
163 |
164 | Automatic Interactive Docs (Swagger UI): http://localhost.app.camel-ai.org/docs
165 |
166 | Automatic Alternative Docs (ReDoc): http://localhost.app.camel-ai.org/redoc
167 |
168 | Adminer: http://localhost.app.camel-ai.org:8080
169 |
170 | Traefik UI: http://localhost.app.camel-ai.org:8090
171 |
172 | ## License
173 | (To be added: License information)
174 |
--------------------------------------------------------------------------------
/app/components/Todo.tsx:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/.dockerignore:
--------------------------------------------------------------------------------
1 | # Python
2 | __pycache__
3 | app.egg-info
4 | *.pyc
5 | .mypy_cache
6 | .coverage
7 | htmlcov
8 | .venv
9 |
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | app.egg-info
3 | *.pyc
4 | .mypy_cache
5 | .coverage
6 | htmlcov
7 | .cache
8 | .venv
9 |
--------------------------------------------------------------------------------
/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM tiangolo/uvicorn-gunicorn-fastapi:python3.10
2 |
3 | WORKDIR /app/
4 |
5 | # Install Poetry
6 | RUN curl -sSL https://install.python-poetry.org | POETRY_HOME=/opt/poetry python && \
7 | cd /usr/local/bin && \
8 | ln -s /opt/poetry/bin/poetry && \
9 | poetry config virtualenvs.create false
10 |
11 | # Copy poetry.lock* in case it doesn't exist in the repo
12 | COPY ./pyproject.toml ./poetry.lock* /app/
13 |
14 | # Allow installing dev dependencies to run tests
15 | ARG INSTALL_DEV=false
16 | RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry install --no-root ; else poetry install --no-root --only main ; fi"
17 |
18 | ENV PYTHONPATH=/app
19 |
20 | COPY ./scripts/ /app/
21 |
22 | COPY ./tests-start.sh /app/
23 |
24 | COPY ./app /app/app
25 |
26 | CMD ["fastapi", "run", "/app/app/main.py"]
27 |
--------------------------------------------------------------------------------
/backend/app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/camel-ai/camel_web_app/896153609faf66b6e3f8040ca109b4993ac0783c/backend/app/__init__.py
--------------------------------------------------------------------------------
/backend/app/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/camel-ai/camel_web_app/896153609faf66b6e3f8040ca109b4993ac0783c/backend/app/api/__init__.py
--------------------------------------------------------------------------------
/backend/app/api/camel_agent.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, HTTPException
2 | from pydantic import BaseModel
3 | from camel.agents import ChatAgent
4 | from camel.configs import ChatGPTConfig
5 | from camel.models import ModelFactory
6 | from camel.types import ModelPlatformType, ModelType
7 | import os
8 | from dotenv import load_dotenv
9 |
10 | load_dotenv()
11 |
12 | router = APIRouter()
13 |
14 | class AgentRequest(BaseModel):
15 | system_message: str
16 | user_message: str
17 | platform_type: str = "OPENAI" # 默认使用 OpenAI
18 | model_type: str = "GPT_4" # 默认使用 GPT-4
19 | api_key: str | None = None
20 | base_url: str | None = None
21 |
22 | class AgentResponse(BaseModel):
23 | content: str
24 |
25 | @router.post("/create-agent", response_model=AgentResponse)
26 | async def create_agent(request: AgentRequest):
27 | try:
28 | # 使用环境变量中的 API key,如果请求中没有提供的话
29 | api_key = request.api_key or os.getenv("OPENAI_API_KEY")
30 | if not api_key:
31 | raise HTTPException(status_code=400, detail="API key is required")
32 |
33 | # 创建模型
34 | model = ModelFactory.create(
35 | model_platform=ModelPlatformType[request.platform_type],
36 | model_type=ModelType[request.model_type],
37 | api_key=api_key,
38 | url=request.base_url,
39 | model_config_dict=ChatGPTConfig(temperature=0.0).as_dict(),
40 | )
41 |
42 | # 创建 agent
43 | agent = ChatAgent(
44 | system_message=request.system_message,
45 | model=model,
46 | message_window_size=10
47 | )
48 |
49 | # 获取响应
50 | response = agent.step(request.user_message)
51 |
52 | return AgentResponse(content=response.msgs[0].content)
53 |
54 | except Exception as e:
55 | raise HTTPException(status_code=500, detail=str(e))
--------------------------------------------------------------------------------
/backend/app/api/deps.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/camel-ai/camel_web_app/896153609faf66b6e3f8040ca109b4993ac0783c/backend/app/api/deps.py
--------------------------------------------------------------------------------
/backend/app/api/main.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter
2 |
3 | from app.api.routes import (
4 | settings,
5 | rag,
6 | workflow,
7 | approval,
8 | mock_data,
9 | human,
10 | agent,
11 | rolePlaying,
12 | camel,
13 | )
14 |
15 | api_router = APIRouter()
16 | api_router.include_router(settings.router, prefix="/settings", tags=["settings"])
17 | api_router.include_router(rag.router, prefix="/rag", tags=["rag"])
18 | api_router.include_router(workflow.router, prefix="/workflow", tags=["workflow"])
19 | api_router.include_router(approval.router, prefix="/approval", tags=["approval"])
20 | api_router.include_router(mock_data.router, prefix="/mock_data", tags=["mock_data"])
21 | api_router.include_router(human.router, prefix="/human", tags=["human"])
22 | api_router.include_router(agent.router, prefix="/agent", tags=["agent"])
23 | api_router.include_router(rolePlaying.router, prefix="/rolePlaying", tags=["rolePlaying"])
24 | api_router.include_router(camel.router, prefix="/camel", tags=["camel"])
25 |
--------------------------------------------------------------------------------
/backend/app/api/routes/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/camel-ai/camel_web_app/896153609faf66b6e3f8040ca109b4993ac0783c/backend/app/api/routes/__init__.py
--------------------------------------------------------------------------------
/backend/app/api/routes/agent.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, HTTPException
2 | from app.models.chat import ChatRequest
3 | from app.models.settings import Settings
4 | from typing import List, Dict, Any
5 | from camel.agents import ChatAgent
6 | from camel.configs import ChatGPTConfig
7 | from camel.models import ModelFactory
8 | from camel.types import ModelPlatformType, ModelType
9 |
10 | router = APIRouter()
11 |
12 | @router.post("/generate_code")
13 | async def generate_code(request_data: Settings):
14 | return generate_code_fun(request_data)
15 |
16 | def generate_code_fun(code: str) -> str:
17 | code = ""
18 | return code
19 |
20 | @router.post("/chat", response_model=Dict[str, Any])
21 | async def chat(request: ChatRequest):
22 | try:
23 | # 创建模型配置
24 | model_config = ChatGPTConfig(
25 | temperature=request.temperature,
26 | max_tokens=request.max_tokens
27 | )
28 |
29 | # 创建模型
30 | model = ModelFactory.create(
31 | model_platform=ModelPlatformType[request.platform_type],
32 | model_type=ModelType[request.model_type],
33 | api_key=request.api_key,
34 | url=request.base_url,
35 | model_config_dict=model_config.as_dict(),
36 | )
37 |
38 | # 创建 agent
39 | agent = ChatAgent(
40 | system_message=request.system_message,
41 | model=model,
42 | message_window_size=10
43 | )
44 |
45 | # 获取响应
46 | response = agent.step(request.user_message)
47 |
48 | return {"content": response.msgs[0].content, "role": "assistant"}
49 |
50 | except Exception as e:
51 | raise HTTPException(status_code=500, detail=str(e))
52 |
--------------------------------------------------------------------------------
/backend/app/api/routes/approval.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, HTTPException
2 | from app.models.approval import ApprovalRequest, ApprovalResponse
3 |
4 | router = APIRouter()
5 |
6 | @router.post(
7 | "/approval/approve",
8 | response_model=ApprovalResponse,
9 | summary="Process approval request",
10 | description="Process the approval request based on the request ID and action type (approve or reject)."
11 | )
12 | async def approve_request(request: ApprovalRequest):
13 | """Process approval request"""
14 | # Simulate approval logic
15 | print("Approval request received:", request)
16 | return {"message": "Request approved successfully!"}
17 |
--------------------------------------------------------------------------------
/backend/app/api/routes/camel.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, HTTPException
2 | from pydantic import BaseModel
3 | from camel.agents import ChatAgent
4 | from camel.configs import ChatGPTConfig
5 | from camel.models import ModelFactory
6 | from camel.types import ModelPlatformType, ModelType
7 | from typing import Optional
8 |
9 | router = APIRouter()
10 |
11 | class AgentRequest(BaseModel):
12 | system_message: str
13 | user_message: str
14 | platform_type: str
15 | model_type: str
16 | base_url: Optional[str] = None
17 | api_key: str
18 | temperature: float = 0.7
19 | max_tokens: int = 2000
20 |
21 | class AgentResponse(BaseModel):
22 | content: str
23 | role: str = "assistant"
24 |
25 | @router.post("/chat", response_model=AgentResponse)
26 | async def chat_with_agent(request: AgentRequest) -> AgentResponse:
27 | try:
28 | # 创建模型配置
29 | model_config = ChatGPTConfig(
30 | temperature=request.temperature,
31 | max_tokens=request.max_tokens
32 | )
33 |
34 | # 创建模型
35 | model = ModelFactory.create(
36 | model_platform=ModelPlatformType[request.platform_type],
37 | model_type=ModelType[request.model_type],
38 | api_key=request.api_key,
39 | url=request.base_url,
40 | model_config_dict=model_config.as_dict(),
41 | )
42 |
43 | # 创建 agent
44 | agent = ChatAgent(
45 | system_message=request.system_message,
46 | model=model,
47 | message_window_size=10
48 | )
49 |
50 | # 获取响应
51 | response = agent.step(request.user_message)
52 |
53 | return AgentResponse(content=response.msgs[0].content)
54 |
55 | except Exception as e:
56 | raise HTTPException(status_code=500, detail=str(e))
--------------------------------------------------------------------------------
/backend/app/api/routes/human.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter
2 | from app.models.human import Human
3 |
4 | router = APIRouter()
5 |
6 | @router.post(
7 | "/human/process",
8 | response_model=Human,
9 | summary="Process Human request",
10 | description="Process the Human workflow based on the parameters provided by the frontend."
11 | )
12 | async def process_human(request: Human):
13 | """Process Human request"""
14 | # Simulate Human processing logic
15 | print("Processing human:", request)
16 | return {"message": "Human process completed successfully!"}
17 |
--------------------------------------------------------------------------------
/backend/app/api/routes/mock_data.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI, HTTPException, Request, APIRouter
2 | from pydantic import BaseModel
3 | from typing import List, Dict, Any
4 | from fastapi.responses import JSONResponse
5 | from fastapi.middleware.cors import CORSMiddleware
6 |
7 | from app.models.settings import Settings
8 | from app.models.chat import ChatRequest
9 |
10 | router = APIRouter()
11 |
12 | # 模拟的数据库数据
13 | MOCK_DATA = {
14 | "platformOptions": [
15 | {"value": "OPENAI", "label": "OpenAI"},
16 | {"value": "MISTRALAI", "label": "MistralAI"},
17 | {"value": "ANTHROPIC", "label": "Anthropic"},
18 | {"value": "QWEN", "label": "Qwen"},
19 | {"value": "DEEPSEEK", "label": "DeepSeek"},
20 | ],
21 | "modelOptions": {
22 | "OPENAI": [
23 | {"value": "GPT_4o", "label": "GPT-4o"},
24 | {"value": "GPT_4o_mini", "label": "GPT-4o-mini"},
25 | {"value": "o1", "label": "o1"},
26 | {"value": "o1_preview", "label": "o1-preview"},
27 | {"value": "o1_mini", "label": "o1-mini"},
28 | {"value": "GPT_4_TURBO", "label": "GPT-4-turbo"},
29 | {"value": "GPT_4", "label": "GPT-4"},
30 | {"value": "GPT_3_5_TURBO", "label": "GPT-3.5-Turbo"},
31 | ],
32 | "MISTRALAI": [
33 | {"value": "MISTRAL_LARGE_2", "label": "Mistral-large-2"},
34 | {"value": "MISTRAL_12B_2409", "label": "Mistral-12b-2409"},
35 | {"value": "MISTRAL_8B_LATEST", "label": "Mistral-8b-latest"},
36 | {"value": "MISTRAL_3B_LATEST", "label": "Mistral-3b-latest"},
37 | {"value": "OPEN_MISTRAL_7B", "label": "Open-mistral-7b"},
38 | {"value": "OPEN_MISTRAL_NEMO", "label": "Open-mistral-nemo"},
39 | {"value": "CODESTRAL", "label": "Codestral"},
40 | {"value": "OPEN_MIXTRAL_8X7B", "label": "Open-mixtral-8x7b"},
41 | {"value": "OPEN_MIXTRAL_8X22B", "label": "Open-mixtral-8x22b"},
42 | {"value": "OPEN_CODESTRAL_MAMBA", "label": "Open-codestral-mamba"},
43 | ],
44 | "ANTHROPIC": [
45 | {"value": "CLAUDE_3_5_SONNET_LATEST", "label": "Claude-3-5-Sonnet-latest"},
46 | {"value": "CLAUDE_3_5_HAIKU_LATEST", "label": "Claude-3-5-haiku-latest"},
47 | {"value": "CLAUDE_3_HAIKU_20240307", "label": "Claude-3-haiku-20240307"},
48 | {"value": "CLAUDE_3_SONNET_20240229", "label": "Claude-3-sonnet-20240229"},
49 | {"value": "CLAUDE_3_OPUS_LATEST", "label": "Claude-3-opus-latest"},
50 | {"value": "CLAUDE_2_0", "label": "Claude-2.0"},
51 | ],
52 | "QWEN": [
53 | {"value": "QWEN_32b_preview", "label": "Qwen-32b-preview"},
54 | {"value": "QWEN_MAX", "label": "Qwen-max"},
55 | {"value": "QWEN_PLUS", "label": "Qwen-plus"},
56 | {"value": "QWEN_TURBO", "label": "Qwen-turbo"},
57 | {"value": "QWEN_LONG", "label": "Qwen-long"},
58 | {"value": "QWEN_VL_MAX", "label": "Qwen-vl-max"},
59 | {"value": "QWEN_MATH_PLUS", "label": "Qwen-math-plus"},
60 | {"value": "QWEN_MATH_TURBO", "label": "Qwen-math-turbo"},
61 | {"value": "QWEN_CODER_TURBO", "label": "Qwen-coder-turbo"},
62 | {"value": "QWEN2_5_CODER_32B_INSTRUCT", "label": "Qwen2.5-coder-32b-instruct"},
63 | {"value": "QWEN2_5_72B_INSTRUCT", "label": "Qwen2.5-72b-instruct"},
64 | {"value": "QWEN2_5_32B_INSTRUCT", "label": "Qwen2.5-32b-instruct"},
65 | {"value": "QWEN2_5_14B_INSTRUCT", "label": "Qwen2.5-14b-instruct"},
66 | ],
67 | "DEEPSEEK": [
68 | {"value": "DEEPSEEK_CHAT", "label": "DeepSeek-chat"},
69 | {"value": "DEEPSEEK_REASONER", "label": "DeepSeek-reasoner"},
70 | ],
71 | },
72 | "languageOptions": [
73 | {"value": "English", "label": "English"},
74 | {"value": "Chinese", "label": "中文"},
75 | {"value": "Japanese", "label": "日本語"},
76 | {"value": "Korean", "label": "한국어"},
77 | ],
78 | "approvalHistory": [
79 | {
80 | "id": 1,
81 | "tool": "File System Access",
82 | "timestamp": "2024-03-20 10:30:45",
83 | "status": "approved",
84 | "risk": "low",
85 | },
86 | {
87 | "id": 2,
88 | "tool": "Database Write",
89 | "timestamp": "2024-03-20 11:15:22",
90 | "status": "rejected",
91 | "risk": "high",
92 | },
93 | ],
94 | "pendingApprovals": [
95 | {
96 | "id": 1,
97 | "tool": "File System Access",
98 | "risk": "high",
99 | "description": "Request to access sensitive file: /data/users.db",
100 | "requestedBy": "AI Agent",
101 | "timestamp": "2024-03-21 15:30:00",
102 | "context": {
103 | "purpose": "Data analysis",
104 | "impact": "High - Involves sensitive user data",
105 | },
106 | },
107 | {
108 | "id": 2,
109 | "tool": "External API Call",
110 | "risk": "medium",
111 | "description": "Request to call external API: api.example.com",
112 | "requestedBy": "AI Agent",
113 | "timestamp": "2024-03-21 15:28:00",
114 | "context": {
115 | "purpose": "Data verification",
116 | "impact": "Medium - External service interaction",
117 | },
118 | },
119 | ],
120 | "recentActivity": [
121 | {
122 | "id": 1,
123 | "type": "approval",
124 | "timestamp": "2024-03-21 15:35:00",
125 | "description": "Approved database query request",
126 | },
127 | {
128 | "id": 2,
129 | "type": "rejection",
130 | "timestamp": "2024-03-21 15:20:00",
131 | "description": "Rejected unauthorized file access attempt",
132 | },
133 | ],
134 | }
135 |
136 |
137 | # 获取所有选项和数据
138 | @router.get("/api/getOptions", response_model=Dict[str, Any])
139 | async def get_options():
140 | return MOCK_DATA
141 |
142 | # 处理聊天请求
143 | @router.post("/api/chat", response_model=Dict[str, Any])
144 | async def chat(request:ChatRequest,settings: Settings):
145 | print(request)
146 | return {"message": "Hello, this is a mock response."}
147 |
--------------------------------------------------------------------------------
/backend/app/api/routes/rag.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, UploadFile, File, HTTPException
2 | from app.models.rag import RAGRequest, RAGResponse
3 | from app.models.chat import ChatRequest
4 | from typing import List, Dict, Any
5 |
6 | router = APIRouter()
7 |
8 | @router.post(
9 | "/rag/process",
10 | response_model=RAGResponse,
11 | summary="Process RAG request",
12 | description="Process the RAG workflow based on parameters provided by the frontend (such as document sources, embedding models, vector storage, etc.)."
13 | )
14 | async def process_rag(request: RAGRequest):
15 | """Process RAG request"""
16 | # Simulate RAG processing logic
17 | print("Processing RAG:", request)
18 | return {"message": "RAG process completed successfully!"}
19 |
20 | @router.post(
21 | "/rag/upload",
22 | response_model=RAGResponse,
23 | summary="Upload document",
24 | description="Upload documents to support the RAG workflow. Supported file formats include PDF, TXT, DOC, DOCX."
25 | )
26 | async def upload_documents(file: UploadFile = File(...)):
27 | """Upload document"""
28 | # Simulate document upload logic
29 | print("File uploaded:", file.filename)
30 | return {"message": "Document uploaded successfully!"}
31 |
32 | @router.post("/rag/chat", response_model=Dict[str, Any])
33 | async def chat(request: ChatRequest, settings: RAGRequest):
34 | print(request)
35 | return {"message": "Hello, this is a mock response."}
36 |
--------------------------------------------------------------------------------
/backend/app/api/routes/rolePlaying.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter
2 | from app.models.chat import ChatRequest
3 | from app.models.settings import Settings
4 | from typing import List, Dict, Any
5 | router = APIRouter()
6 |
7 |
8 | @router.post("/rolePlaying/generate_code")
9 | async def generate_code(request_data: Settings):
10 | return generate_code_fun(request_data)
11 |
12 | def generate_code_fun(code: str) -> str:
13 | code = ""
14 | return code
15 |
16 |
17 |
18 | @router.post("/rolePlaying/chat", response_model=Dict[str, Any])
19 | async def chat(request:ChatRequest,assistantAgent: Settings,userAgent: Settings):
20 | print(request)
21 | return {"message": "Hello, this is a mock response."}
22 |
--------------------------------------------------------------------------------
/backend/app/api/routes/settings.py:
--------------------------------------------------------------------------------
1 | from typing import List, Dict
2 |
3 | from fastapi import APIRouter, HTTPException
4 | from app.models.settings import Settings, SettingsResponse,CodeGenerateRequest
5 | from pydantic import BaseModel
6 | import re
7 |
8 | router = APIRouter()
9 |
10 | @router.post("/settings/generate_code")
11 | async def generate_code(request_data: CodeGenerateRequest):
12 | module_type = request_data.moduleType
13 | settings = request_data.settings
14 |
15 | def get_module_code(module_type: str, settings: Settings):
16 | if module_type == "Module1":
17 | return generate_module1_code(settings)
18 | elif module_type == "Module2":
19 | return generate_module2_code(settings)
20 | elif module_type == "Module5":
21 | return generate_module5_code(settings)
22 | else:
23 | return "// Module not implemented"
24 |
25 | def generate_module1_code(settings: Settings):
26 | return f"""
27 | from camel.agents import ChatAgent
28 | from camel.configs import ChatGPTConfig
29 | from camel.models import ModelFactory
30 | from camel.types import ModelPlatformType, ModelType
31 |
32 | model = ModelFactory.create(
33 | model_platform=ModelPlatformType.{settings.platformType},
34 | model_type=ModelType.{settings.modelType},
35 | model_config_dict=ChatGPTConfig(temperature=0.0).as_dict(),
36 | )
37 |
38 | agent = ChatAgent(model=model, system_message="{settings.systemMessage}")
39 | response = agent.step("{settings.systemMessage}")
40 | print(response.msgs[0].content)
41 | """
42 |
43 | def generate_module2_code(settings: Settings):
44 | return f"""
45 | from camel.agents import RolePlaying
46 | from camel.configs import ChatGPTConfig
47 | from camel.models import ModelFactory
48 |
49 | role_playing = RolePlaying(
50 | assistant_role_name="{settings.assistantRole}",
51 | user_role_name="{settings.userRole}",
52 | task_prompt="{settings.taskPrompt}",
53 | output_language="{settings.outputLanguage}",
54 | model_config=ChatGPTConfig(temperature=0.7)
55 | )
56 |
57 | response = role_playing.start()
58 | print(response.assistant_message)
59 | """
60 |
61 | def generate_module5_code(settings: Settings):
62 | return f"""
63 | from camel.embeddings import OpenAIEmbedding
64 | from camel.retrievers import AutoRetriever
65 | from camel.storages import QdrantStorage
66 | from camel.models import ModelFactory
67 |
68 | embedding_model = OpenAIEmbedding(model_name="{settings.embeddingModel}")
69 | vector_store = QdrantStorage.create(embedding_model=embedding_model)
70 |
71 | retriever = AutoRetriever(
72 | vector_store=vector_store,
73 | top_k={settings.retrievalParams.get('topK', 3)},
74 | similarity_threshold={settings.retrievalParams.get('threshold', 0.7)}
75 | )
76 |
77 | response = retriever.retrieve(query="")
78 | print(response)
79 | """
80 |
81 | code = get_module_code(module_type, settings)
82 | # 安全检查:避免代码注入
83 | code = sanitize_code(code)
84 | return {"codeExample": code}
85 |
86 | def sanitize_code(code: str) -> str:
87 | # 移除潜在的恶意代码
88 | code = re.sub(r'[\w\s]*import[\s\w]*', '', code, flags=re.IGNORECASE)
89 | code = re.sub(r'eval\([\w\s,\'"]*\)', '', code, flags=re.IGNORECASE)
90 | code = re.sub(r'exec\([\w\s,\'"]*\)', '', code, flags=re.IGNORECASE)
91 | code = re.sub(r'os\.([\w\.]*)', '', code, flags=re.IGNORECASE)
92 | return code
93 |
--------------------------------------------------------------------------------
/backend/app/api/routes/workflow.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, HTTPException
2 | from app.models.workflow import WorkflowRequest, WorkflowResponse
3 |
4 | router = APIRouter()
5 |
6 | @router.post(
7 | "/workflow/start",
8 | response_model=WorkflowResponse,
9 | summary="Start Workflow",
10 | description="Start the workflow based on the user-configured agents and task definitions."
11 | )
12 | async def start_workflow(request: WorkflowRequest):
13 | """Start Workflow"""
14 | # Simulate workflow processing logic
15 | print("Starting workflow:", request)
16 | return {"message": "Workflow started successfully!"}
17 |
--------------------------------------------------------------------------------
/backend/app/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/camel-ai/camel_web_app/896153609faf66b6e3f8040ca109b4993ac0783c/backend/app/core/__init__.py
--------------------------------------------------------------------------------
/backend/app/core/config.py:
--------------------------------------------------------------------------------
1 | import secrets
2 | import warnings
3 | from typing import Annotated, Any, Literal, Union, Optional
4 |
5 | from pydantic import (
6 | AnyUrl,
7 | BeforeValidator,
8 | HttpUrl,
9 | computed_field,
10 | model_validator,
11 | )
12 | from pydantic_settings import BaseSettings, SettingsConfigDict
13 | from typing_extensions import Self
14 |
15 |
16 | def parse_cors(v: Any) -> Union[list[str], str]:
17 | if isinstance(v, str) and not v.startswith("["):
18 | return [i.strip() for i in v.split(",")]
19 | elif isinstance(v, (list, str)):
20 | return v
21 | raise ValueError(v)
22 |
23 |
24 | class Settings(BaseSettings):
25 | model_config = SettingsConfigDict(
26 | env_file=".env", env_ignore_empty=True, extra="ignore"
27 | )
28 | API_V1_STR: str = "/api/v1"
29 | SECRET_KEY: str = secrets.token_urlsafe(32)
30 | # 60 minutes * 24 hours * 8 days = 8 days
31 | ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
32 | DOMAIN: str = "localhost"
33 | ENVIRONMENT: Literal["local", "staging", "production"] = "local"
34 |
35 | @computed_field # type: ignore[prop-decorator]
36 | @property
37 | def server_host(self) -> str:
38 | # Use HTTPS for anything other than local development
39 | if self.ENVIRONMENT == "local":
40 | return f"http://{self.DOMAIN}"
41 | return f"https://{self.DOMAIN}"
42 |
43 | BACKEND_CORS_ORIGINS: Annotated[
44 | Union[list[AnyUrl], str], BeforeValidator(parse_cors)
45 | ] = []
46 |
47 | PROJECT_NAME: str = "CAMEL Web App"
48 | SENTRY_DSN: HttpUrl | None = None
49 |
50 | def _check_default_secret(self, var_name: str, value: Optional[str]) -> None:
51 | if value == "changethis":
52 | message = (
53 | f'The value of {var_name} is "changethis", '
54 | "for security, please change it, at least for deployments."
55 | )
56 | if self.ENVIRONMENT == "local":
57 | warnings.warn(message, stacklevel=1)
58 | else:
59 | raise ValueError(message)
60 |
61 | @model_validator(mode="after")
62 | def _enforce_non_default_secrets(self) -> Self:
63 | self._check_default_secret("SECRET_KEY", self.SECRET_KEY)
64 |
65 | return self
66 |
67 |
68 | settings = Settings() # type: ignore
69 |
--------------------------------------------------------------------------------
/backend/app/main.py:
--------------------------------------------------------------------------------
1 | import sentry_sdk
2 | from fastapi import FastAPI
3 | from fastapi.routing import APIRoute
4 | from starlette.middleware.cors import CORSMiddleware
5 |
6 | from app.api.main import api_router
7 | from app.core.config import settings
8 |
9 |
10 | def custom_generate_unique_id(route: APIRoute) -> str:
11 | return f"{route.tags[0]}-{route.name}"
12 |
13 |
14 | if settings.SENTRY_DSN and settings.ENVIRONMENT != "local":
15 | sentry_sdk.init(dsn=str(settings.SENTRY_DSN), enable_tracing=True)
16 |
17 | app = FastAPI(
18 | title=settings.PROJECT_NAME,
19 | openapi_url=f"{settings.API_V1_STR}/openapi.json",
20 | generate_unique_id_function=custom_generate_unique_id,
21 | )
22 |
23 | # Set all CORS enabled origins
24 | if settings.BACKEND_CORS_ORIGINS:
25 | app.add_middleware(
26 | CORSMiddleware,
27 | allow_origins=[
28 | str(origin).strip("/") for origin in settings.BACKEND_CORS_ORIGINS
29 | ],
30 | allow_credentials=True,
31 | allow_methods=["*"],
32 | allow_headers=["*"],
33 | )
34 |
35 | app.include_router(api_router, prefix=settings.API_V1_STR)
36 |
--------------------------------------------------------------------------------
/backend/app/models/__init__.py:
--------------------------------------------------------------------------------
1 | __all__ = []
2 |
--------------------------------------------------------------------------------
/backend/app/models/approval.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel, Field
2 |
3 | class ApprovalRequest(BaseModel):
4 | """
5 | 审批请求模型,包含审批请求的ID和操作类型。
6 | """
7 | id: int = Field(..., description="The ID of the approval request.")
8 | action: str = Field(..., description="The action to be taken on the request (e.g., 'approve', 'reject').")
9 |
10 | class ApprovalResponse(BaseModel):
11 | """
12 | 审批响应模型,返回审批结果。
13 | """
14 | message: str = Field(..., description="The result message of the approval process.")
15 |
--------------------------------------------------------------------------------
/backend/app/models/chat.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel, Field
2 | from typing import Optional
3 |
4 |
5 | class ChatRequest(BaseModel):
6 | user_message: str
7 | system_message: str
8 | platform_type: str = "OPENAI"
9 | model_type: str = "GPT_4"
10 | base_url: Optional[str] = None
11 | api_key: str
12 | temperature: float = 0.7
13 | max_tokens: int = 2000
14 |
--------------------------------------------------------------------------------
/backend/app/models/human.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel, Field
2 |
3 | class Human(BaseModel):
4 | apiKey: str
5 | timeout: str
6 | level: str
7 | email: bool
8 | browser: bool
9 | slack: bool
10 | fileSystemAccess: str
11 | externalAPICall: str
12 |
--------------------------------------------------------------------------------
/backend/app/models/rag.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel, Field
2 |
3 | class RAGRequest(BaseModel):
4 | """
5 | RAG请求模型,包含处理RAG流程所需的所有参数。
6 | """
7 | RAGType: str = Field(..., description="The type of RAG process to be performed (e.g., 'embedding', 'graph').")
8 | fileUrl: str = Field(..., description="The URL of the documents to be processed.")
9 | documentSource: str = Field(..., description="The source of the documents to be processed (e.g., file path or URL).")
10 | embeddingModel: str = Field(..., description="The embedding model used for document processing (e.g., text-embedding-3-small).")
11 | vectorStore: str = Field(..., description="The vector store used for storing document embeddings (e.g., FAISS, Qdrant).")
12 | retrievalParams: dict = Field(..., description="Parameters for document retrieval (e.g., topK, threshold).")
13 | graphDbConfig: dict = Field(..., description="Configuration for the graph database (e.g., Neo4j URI, username, password).")
14 | topNumber: int = Field(..., description="The number of documents to retrieve.")
15 | class RAGResponse(BaseModel):
16 | """
17 | RAG响应模型,返回处理结果。
18 | """
19 | message: str = Field(..., description="The result message of the RAG process.")
20 |
--------------------------------------------------------------------------------
/backend/app/models/settings.py:
--------------------------------------------------------------------------------
1 | from typing import Dict
2 |
3 | from pydantic import BaseModel, Field
4 |
5 | class Settings(BaseModel):
6 | """
7 | 用户设置模型,包含前端界面中可配置的各项参数。
8 | """
9 | availableToolkits: list = Field(default=[], description="List of available toolkits for the user to choose from.")
10 | platformType: str = Field(..., description="The platform type selected by the user (e.g., OPENAI, MISTRALAI).")
11 | modelType: str = Field(..., description="The model type selected by the user (e.g., GPT_4, MISTRAL_LARGE_2).")
12 | apiKey: str = Field(..., description="The API key for authentication with the selected platform.")
13 | baseURL: str = Field(..., description="The base URL for the selected platform.")
14 | yourApiKey: str = Field(default="", description="The API key for authentication with the selected platform.")
15 | yourBaseURL: str = Field(default="", description="The base URL for the selected platform.")
16 | yourPlatformType: str = Field(default="", description="The platform type selected by the user (e.g., OPENAI, MISTRALAI).")
17 | yourModelType: str = Field(default="", description="The model type selected by the user (e.g., GPT_4, MISTRAL_LARGE_2).")
18 | systemMessage: str = Field(..., description="The system message that defines the behavior of the AI agent.")
19 | outputLanguage: str = Field(..., description="The output language for the AI agent's responses.")
20 | temperature: float = Field(..., description="The temperature parameter for the AI agent.")
21 | agents: list = Field(default=[], description="List of agents configured by the user.")
22 | max_tokens: int = Field(..., description="The maximum number of tokens for the AI agent's responses.")
23 | pendingApprovals: list = Field(default=[], description="List of pending approval requests.")
24 | recentActivity: list = Field(default=[], description="List of recent activity logs.")
25 | retrievalParams: Dict
26 |
27 | class SettingsResponse(BaseModel):
28 | """
29 | 设置响应模型,返回保存的设置数据。
30 | """
31 | data: Settings
32 |
33 | class CodeGenerateRequest(BaseModel):
34 | moduleType: str
35 | settings: Settings
36 |
37 |
--------------------------------------------------------------------------------
/backend/app/models/workflow.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel, Field
2 |
3 | class WorkflowRequest(BaseModel):
4 | """
5 | 工作流请求模型,包含启动工作流所需的所有参数。
6 | """
7 | agents: list = Field(..., description="List of agents configured for the workflow.")
8 | taskDefinition: str = Field(..., description="The definition of the task to be executed by the workflow.")
9 |
10 | class WorkflowResponse(BaseModel):
11 | """
12 | 工作流响应模型,返回启动结果。
13 | """
14 | message: str = Field(..., description="The result message of the workflow process.")
15 |
--------------------------------------------------------------------------------
/backend/app/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/camel-ai/camel_web_app/896153609faf66b6e3f8040ca109b4993ac0783c/backend/app/tests/__init__.py
--------------------------------------------------------------------------------
/backend/app/tests/conftest.py:
--------------------------------------------------------------------------------
1 | from collections.abc import Generator
2 |
3 | import pytest
4 | from fastapi.testclient import TestClient
5 | from app.main import app
6 |
7 |
8 | @pytest.fixture(scope="module")
9 | def client() -> Generator[TestClient, None, None]:
10 | with TestClient(app) as c:
11 | yield c
12 |
--------------------------------------------------------------------------------
/backend/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "camel-web-app"
3 | version = "0.1.0"
4 | description = "A web application for CAMEL"
5 | readme = "README.md"
6 | package-mode = false
7 |
8 | [tool.poetry.dependencies]
9 | python = ">=3.10, <3.13"
10 | uvicorn = {extras = ["standard"], version = "^0.24.0.post1"}
11 | fastapi = {extras = ["standard"], version = "^0.115.11"}
12 | python-multipart = "<1.0.0,>=0.0.7"
13 | tenacity = "^8.2.3"
14 | pydantic = ">2.0"
15 |
16 | gunicorn = "^22.0.0"
17 | # Pin bcrypt until passlib supports the latest
18 | pydantic-settings = "^2.2.1"
19 | sentry-sdk = {extras = ["fastapi"], version = "^1.40.6"}
20 | camel-ai = {extras = ["all"], version = "^0.2.38"}
21 |
22 | [tool.poetry.group.dev.dependencies]
23 | pytest = "^7.4.3"
24 | mypy = "^1.8.0"
25 | ruff = "^0.2.2"
26 | pre-commit = "^3.6.2"
27 | types-passlib = "^1.7.7.20240106"
28 | coverage = "^7.4.3"
29 |
30 | [build-system]
31 | requires = ["poetry>=0.12"]
32 | build-backend = "poetry.masonry.api"
33 |
34 | [tool.mypy]
35 | strict = true
36 | exclude = ["venv", ".venv", "alembic"]
37 |
38 | [tool.ruff]
39 | target-version = "py310"
40 | exclude = ["alembic"]
41 |
42 | [tool.ruff.lint]
43 | select = [
44 | "E", # pycodestyle errors
45 | "W", # pycodestyle warnings
46 | "F", # pyflakes
47 | "I", # isort
48 | "B", # flake8-bugbear
49 | "C4", # flake8-comprehensions
50 | "UP", # pyupgrade
51 | "ARG001", # unused arguments in functions
52 | ]
53 | ignore = [
54 | "E501", # line too long, handled by black
55 | "B008", # do not perform function calls in argument defaults
56 | "W191", # indentation contains tabs
57 | "B904", # Allow raising exceptions without from e, for HTTPException
58 | ]
59 |
60 | [tool.ruff.lint.pyupgrade]
61 | # Preserve types, even if a file imports `from __future__ import annotations`.
62 | keep-runtime-typing = true
63 |
--------------------------------------------------------------------------------
/backend/scripts/format.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -e
2 | set -x
3 |
4 | ruff check app scripts --fix
5 | ruff format app scripts
6 |
--------------------------------------------------------------------------------
/backend/scripts/lint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 | set -x
5 |
6 | mypy app
7 | ruff check app
8 | ruff format app --check
9 |
--------------------------------------------------------------------------------
/backend/scripts/test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 | set -x
5 |
6 | coverage run --source=app -m pytest
7 | coverage report --show-missing
8 | coverage html --title "${@-coverage}"
9 |
--------------------------------------------------------------------------------
/backend/tests-start.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 | set -e
3 | set -x
4 |
5 | bash ./scripts/test.sh "$@"
6 |
--------------------------------------------------------------------------------
/docker-compose.override.yml:
--------------------------------------------------------------------------------
1 | services:
2 |
3 | proxy:
4 | image: traefik:3.0
5 | volumes:
6 | - /var/run/docker.sock:/var/run/docker.sock
7 | ports:
8 | - "80:80"
9 | - "8090:8080"
10 | # Duplicate the command from docker-compose.yml to add --api.insecure=true
11 | command:
12 | # Enable Docker in Traefik, so that it reads labels from Docker services
13 | - --providers.docker
14 | # Add a constraint to only use services with the label for this stack
15 | - --providers.docker.constraints=Label(`traefik.constraint-label`, `traefik-public`)
16 | # Do not expose all Docker services, only the ones explicitly exposed
17 | - --providers.docker.exposedbydefault=false
18 | # Create an entrypoint "http" listening on port 80
19 | - --entrypoints.http.address=:80
20 | # Create an entrypoint "https" listening on port 443
21 | - --entrypoints.https.address=:443
22 | # Enable the access log, with HTTP requests
23 | - --accesslog
24 | # Enable the Traefik log, for configurations and errors
25 | - --log
26 | # Enable debug logging for local development
27 | - --log.level=DEBUG
28 | # Enable the Dashboard and API
29 | - --api
30 | # Enable the Dashboard and API in insecure mode for local development
31 | - --api.insecure=true
32 | labels:
33 | # Enable Traefik for this service, to make it available in the public network
34 | - traefik.enable=true
35 | - traefik.constraint-label=traefik-public
36 | # Dummy https-redirect middleware that doesn't really redirect, only to
37 | # allow running it locally
38 | - traefik.http.middlewares.https-redirect.contenttype.autodetect=false
39 | networks:
40 | - traefik-public
41 | - default
42 |
43 | db:
44 | restart: "no"
45 | ports:
46 | - "5432:5432"
47 |
48 | adminer:
49 | restart: "no"
50 | ports:
51 | - "8080:8080"
52 |
53 | celery-worker:
54 | restart: "no"
55 | volumes:
56 | - ./backend/:/app
57 |
58 | celery-dashboard:
59 | restart: "no"
60 | ports:
61 | - "5556:5555"
62 |
63 | backend:
64 | restart: "no"
65 | ports:
66 | - "8000:80"
67 | volumes:
68 | - ./backend/:/app
69 | build:
70 | context: ./backend
71 | args:
72 | INSTALL_DEV: ${INSTALL_DEV-true}
73 | # command: sleep infinity # Infinite loop to keep container alive doing nothing
74 | command: /start-reload.sh
75 |
76 | frontend:
77 | restart: "no"
78 | ports:
79 | - "5173:80"
80 | build:
81 | context: ./frontend
82 | args:
83 | - VITE_API_URL=http://localhost:8000
84 | - NODE_ENV=development
85 |
86 | networks:
87 | traefik-public:
88 | # For local dev, don't expect an external Traefik network
89 | external: false
90 |
--------------------------------------------------------------------------------
/docker-compose.traefik.yml:
--------------------------------------------------------------------------------
1 | services:
2 | traefik:
3 | image: traefik:3.0
4 | ports:
5 | # Listen on port 80, default for HTTP, necessary to redirect to HTTPS
6 | - 80:80
7 | # Listen on port 443, default for HTTPS
8 | - 443:443
9 | restart: always
10 | labels:
11 | # Enable Traefik for this service, to make it available in the public network
12 | - traefik.enable=true
13 | # Use the traefik-public network (declared below)
14 | - traefik.docker.network=traefik-public
15 | # Define the port inside of the Docker service to use
16 | - traefik.http.services.traefik-dashboard.loadbalancer.server.port=8080
17 | # Make Traefik use this domain (from an environment variable) in HTTP
18 | - traefik.http.routers.traefik-dashboard-http.entrypoints=http
19 | - traefik.http.routers.traefik-dashboard-http.rule=Host(`traefik.${DOMAIN?Variable not set}`)
20 | # traefik-https the actual router using HTTPS
21 | - traefik.http.routers.traefik-dashboard-https.entrypoints=https
22 | - traefik.http.routers.traefik-dashboard-https.rule=Host(`traefik.${DOMAIN?Variable not set}`)
23 | - traefik.http.routers.traefik-dashboard-https.tls=true
24 | # Use the "le" (Let's Encrypt) resolver created below
25 | - traefik.http.routers.traefik-dashboard-https.tls.certresolver=le
26 | # Use the special Traefik service api@internal with the web UI/Dashboard
27 | - traefik.http.routers.traefik-dashboard-https.service=api@internal
28 | # https-redirect middleware to redirect HTTP to HTTPS
29 | - traefik.http.middlewares.https-redirect.redirectscheme.scheme=https
30 | - traefik.http.middlewares.https-redirect.redirectscheme.permanent=true
31 | # traefik-http set up only to use the middleware to redirect to https
32 | - traefik.http.routers.traefik-dashboard-http.middlewares=https-redirect
33 | # admin-auth middleware with HTTP Basic auth
34 | # Using the environment variables USERNAME and HASHED_PASSWORD
35 | - traefik.http.middlewares.admin-auth.basicauth.users=${USERNAME?Variable not set}:${HASHED_PASSWORD?Variable not set}
36 | # Enable HTTP Basic auth, using the middleware created above
37 | - traefik.http.routers.traefik-dashboard-https.middlewares=admin-auth
38 | volumes:
39 | # Add Docker as a mounted volume, so that Traefik can read the labels of other services
40 | - /var/run/docker.sock:/var/run/docker.sock:ro
41 | # Mount the volume to store the certificates
42 | - traefik-public-certificates:/certificates
43 | command:
44 | # Enable Docker in Traefik, so that it reads labels from Docker services
45 | - --providers.docker
46 | # Do not expose all Docker services, only the ones explicitly exposed
47 | - --providers.docker.exposedbydefault=false
48 | # Create an entrypoint "http" listening on port 80
49 | - --entrypoints.http.address=:80
50 | # Create an entrypoint "https" listening on port 443
51 | - --entrypoints.https.address=:443
52 | # Create the certificate resolver "le" for Let's Encrypt, uses the environment variable EMAIL
53 | - --certificatesresolvers.le.acme.email=${EMAIL?Variable not set}
54 | # Store the Let's Encrypt certificates in the mounted volume
55 | - --certificatesresolvers.le.acme.storage=/certificates/acme.json
56 | # Use the TLS Challenge for Let's Encrypt
57 | - --certificatesresolvers.le.acme.tlschallenge=true
58 | # Enable the access log, with HTTP requests
59 | - --accesslog
60 | # Enable the Traefik log, for configurations and errors
61 | - --log
62 | # Enable the Dashboard and API
63 | - --api
64 | networks:
65 | # Use the public network created to be shared between Traefik and
66 | # any other service that needs to be publicly available with HTTPS
67 | - traefik-public
68 |
69 | volumes:
70 | # Create a volume to store the certificates, even if the container is recreated
71 | traefik-public-certificates:
72 |
73 | networks:
74 | # Use the previously created public network "traefik-public", shared with other
75 | # services that need to be publicly available via this Traefik
76 | traefik-public:
77 | external: true
78 |
--------------------------------------------------------------------------------
/frontend/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/frontend/.env:
--------------------------------------------------------------------------------
1 | VITE_API_URL=https://api.app.camel-ai.org
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | openapi.json
15 |
16 | # Editor directories and files
17 | .vscode/*
18 | !.vscode/extensions.json
19 | .idea
20 | .DS_Store
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
26 | /test-results/
27 | /playwright-report/
28 | /blob-report/
29 | /playwright/.cache/
30 |
--------------------------------------------------------------------------------
/frontend/.nvmrc:
--------------------------------------------------------------------------------
1 | 20
2 |
--------------------------------------------------------------------------------
/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | # Stage 0, "build-stage", based on Node.js, to build and compile the frontend
2 | FROM node:20 AS build-stage
3 |
4 | WORKDIR /app
5 |
6 | COPY package*.json /app/
7 |
8 | RUN npm install
9 |
10 | COPY ./ /app/
11 |
12 | ARG VITE_API_URL=${VITE_API_URL}
13 |
14 | RUN npm run build
15 |
16 |
17 | # Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
18 | FROM nginx:1
19 |
20 | COPY --from=build-stage /app/dist/ /usr/share/nginx/html
21 |
22 | COPY ./nginx.conf /etc/nginx/conf.d/default.conf
23 | COPY ./nginx-backend-not-found.conf /etc/nginx/extra-conf.d/backend-not-found.conf
24 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # FastAPI Project - Frontend
2 |
3 | The frontend is built with [Vite](https://vitejs.dev/), [React](https://reactjs.org/), [TypeScript](https://www.typescriptlang.org/), [TanStack Query](https://tanstack.com/query), [TanStack Router](https://tanstack.com/router) and [Chakra UI](https://chakra-ui.com/).
4 |
5 | ## Frontend development
6 |
7 | Before you begin, ensure that you have either the Node Version Manager (nvm) or Fast Node Manager (fnm) installed on your system.
8 |
9 | * To install fnm follow the [official fnm guide](https://github.com/Schniz/fnm#installation). If you prefer nvm, you can install it using the [official nvm guide](https://github.com/nvm-sh/nvm#installing-and-updating).
10 |
11 | * After installing either nvm or fnm, proceed to the `frontend` directory:
12 |
13 | ```bash
14 | cd frontend
15 | ```
16 | * If the Node.js version specified in the `.nvmrc` file isn't installed on your system, you can install it using the appropriate command:
17 |
18 | ```bash
19 | # If using fnm
20 | fnm install
21 |
22 | # If using nvm
23 | nvm install
24 | ```
25 |
26 | * Once the installation is complete, switch to the installed version:
27 |
28 | ```bash
29 | # If using fnm
30 | fnm use
31 |
32 | # If using nvm
33 | nvm use
34 | ```
35 |
36 | * Within the `frontend` directory, install the necessary NPM packages:
37 |
38 | ```bash
39 | npm install
40 | ```
41 |
42 | * And start the live server with the following `npm` script:
43 |
44 | ```bash
45 | npm run dev
46 | ```
47 |
48 | * Then open your browser at http://localhost:5173/.
49 |
50 | Notice that this live server is not running inside Docker, it's for local development, and that is the recommended workflow. Once you are happy with your frontend, you can build the frontend Docker image and start it, to test it in a production-like environment. But building the image at every change will not be as productive as running the local development server with live reload.
51 |
52 | Check the file `package.json` to see other available options.
53 |
54 | ### Removing the frontend
55 |
56 | If you are developing an API-only app and want to remove the frontend, you can do it easily:
57 |
58 | * Remove the `./frontend` directory.
59 |
60 | * In the `docker-compose.yml` file, remove the whole service / section `frontend`.
61 |
62 | * In the `docker-compose.override.yml` file, remove the whole service / section `frontend`.
63 |
64 | Done, you have a frontend-less (api-only) app. 🤓
65 |
66 | ---
67 |
68 | If you want, you can also remove the `FRONTEND` environment variables from:
69 |
70 | * `.env`
71 | * `./scripts/*.sh`
72 |
73 | But it would be only to clean them up, leaving them won't really have any effect either way.
74 |
75 | ## Generate Client
76 |
77 | * Start the Docker Compose stack.
78 |
79 | * Download the OpenAPI JSON file from `http://localhost/api/v1/openapi.json` and copy it to a new file `openapi.json` at the root of the `frontend` directory.
80 |
81 | * To simplify the names in the generated frontend client code, modify the `openapi.json` file by running the following script:
82 |
83 | ```bash
84 | node modify-openapi-operationids.js
85 | ```
86 |
87 | * To generate the frontend client, run:
88 |
89 | ```bash
90 | npm run generate-client
91 | ```
92 |
93 | * Commit the changes.
94 |
95 | Notice that everytime the backend changes (changing the OpenAPI schema), you should follow these steps again to update the frontend client.
96 |
97 | ## Using a Remote API
98 |
99 | If you want to use a remote API, you can set the environment variable `VITE_API_URL` to the URL of the remote API. For example, you can set it in the `frontend/.env` file:
100 |
101 | ```env
102 | VITE_API_URL=https://my-remote-api.example.com
103 | ```
104 |
105 | Then, when you run the frontend, it will use that URL as the base URL for the API.
106 |
107 | ## Code Structure
108 |
109 | The frontend code is structured as follows:
110 |
111 | * `frontend/src` - The main frontend code.
112 | * `frontend/src/assets` - Static assets.
113 | * `frontend/src/client` - The generated OpenAPI client.
114 | * `frontend/src/components` - The different components of the frontend.
115 | * `frontend/src/hooks` - Custom hooks.
116 | * `frontend/src/routes` - The different routes of the frontend which include the pages.
117 | * `theme.tsx` - The Chakra UI custom theme.
118 |
119 | ## End-to-End Testing with Playwright
120 |
121 | The frontend includes initial end-to-end tests using Playwright. To run the tests, you need to have the Docker Compose stack running. Start the stack with the following command:
122 |
123 | ```bash
124 | docker compose up -d
125 | ```
126 |
127 | Then, you can run the tests with the following command:
128 |
129 | ```bash
130 | npx playwright test
131 | ```
132 |
133 | You can also run your tests in UI mode to see the browser and interact with it running:
134 |
135 | ```bash
136 | npx playwright test --ui
137 | ```
138 |
139 | To stop and remove the Docker Compose stack and clean the data created in tests, use the following command:
140 |
141 | ```bash
142 | docker compose down -v
143 | ```
144 |
145 | To update the tests, navigate to the tests directory and modify the existing test files or add new ones as needed.
146 |
147 | For more information on writing and running Playwright tests, refer to the official [Playwright documentation](https://playwright.dev/docs/intro).
--------------------------------------------------------------------------------
/frontend/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.6.1/schema.json",
3 | "organizeImports": {
4 | "enabled": true
5 | },
6 | "files": {
7 | "ignore": [
8 | "node_modules",
9 | "src/client/",
10 | "src/routeTree.gen.ts",
11 | "playwright.config.ts",
12 | "playwright-report"
13 | ]
14 | },
15 | "linter": {
16 | "enabled": true,
17 | "rules": {
18 | "recommended": true,
19 | "suspicious": {
20 | "noExplicitAny": "off",
21 | "noArrayIndexKey": "off"
22 | },
23 | "style": {
24 | "noNonNullAssertion": "off"
25 | }
26 | }
27 | },
28 | "formatter": {
29 | "indentStyle": "space"
30 | },
31 | "javascript": {
32 | "formatter": {
33 | "quoteStyle": "double",
34 | "semicolons": "asNeeded"
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/frontend/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
8 | "css": "src/index.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/frontend/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | 'react-hooks': reactHooks,
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
Camel Webapp
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/modify-openapi-operationids.js:
--------------------------------------------------------------------------------
1 | import * as fs from "node:fs"
2 |
3 | async function modifyOpenAPIFile(filePath) {
4 | try {
5 | const data = await fs.promises.readFile(filePath)
6 | const openapiContent = JSON.parse(data)
7 |
8 | const paths = openapiContent.paths
9 | for (const pathKey of Object.keys(paths)) {
10 | const pathData = paths[pathKey]
11 | for (const method of Object.keys(pathData)) {
12 | const operation = pathData[method]
13 | if (operation.tags && operation.tags.length > 0) {
14 | const tag = operation.tags[0]
15 | const operationId = operation.operationId
16 | const toRemove = `${tag}-`
17 | if (operationId.startsWith(toRemove)) {
18 | const newOperationId = operationId.substring(toRemove.length)
19 | operation.operationId = newOperationId
20 | }
21 | }
22 | }
23 | }
24 |
25 | await fs.promises.writeFile(
26 | filePath,
27 | JSON.stringify(openapiContent, null, 2),
28 | )
29 | console.log("File successfully modified")
30 | } catch (err) {
31 | console.error("Error:", err)
32 | }
33 | }
34 |
35 | const filePath = "./openapi.json"
36 | modifyOpenAPIFile(filePath)
37 |
--------------------------------------------------------------------------------
/frontend/nginx-backend-not-found.conf:
--------------------------------------------------------------------------------
1 | location /api {
2 | return 404;
3 | }
4 | location /docs {
5 | return 404;
6 | }
7 | location /redoc {
8 | return 404;
9 | }
10 |
--------------------------------------------------------------------------------
/frontend/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 |
4 | location / {
5 | root /usr/share/nginx/html;
6 | index index.html index.htm;
7 | try_files $uri /index.html =404;
8 | }
9 |
10 | include /etc/nginx/extra-conf.d/*.conf;
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "lint": "biome check --apply-unsafe --no-errors-on-unmatched --files-ignore-unknown=true ./",
10 | "preview": "vite preview",
11 | "generate-client": "openapi-ts --input http://localhost.data.eigent.ai/api/v1/openapi.json --output ./src/client --client axios --exportSchemas true && biome format --write ./src/client"
12 | },
13 | "dependencies": {
14 | "@radix-ui/react-avatar": "^1.1.3",
15 | "@radix-ui/react-checkbox": "^1.1.4",
16 | "@radix-ui/react-collapsible": "^1.1.3",
17 | "@radix-ui/react-dialog": "^1.1.6",
18 | "@radix-ui/react-dropdown-menu": "^2.1.6",
19 | "@radix-ui/react-label": "^2.1.2",
20 | "@radix-ui/react-select": "^2.1.6",
21 | "@radix-ui/react-separator": "^1.1.2",
22 | "@radix-ui/react-slider": "^1.2.3",
23 | "@radix-ui/react-slot": "^1.1.2",
24 | "@radix-ui/react-switch": "^1.1.3",
25 | "@radix-ui/react-tabs": "^1.1.3",
26 | "@radix-ui/react-tooltip": "^1.1.8",
27 | "@tabler/icons-react": "^3.31.0",
28 | "@tailwindcss/vite": "^4.0.17",
29 | "@tanstack/react-query": "^5.28.14",
30 | "@tanstack/react-query-devtools": "^5.28.14",
31 | "@tanstack/react-router": "^1.19.1",
32 | "@types/react-syntax-highlighter": "^15.5.13",
33 | "axios": "1.6.2",
34 | "class-variance-authority": "^0.7.1",
35 | "clsx": "^2.1.1",
36 | "cmdk": "^1.1.1",
37 | "form-data": "4.0.0",
38 | "framer-motion": "^12.6.2",
39 | "lucide-react": "^0.484.0",
40 | "motion": "^12.6.2",
41 | "react": "^18.2.0",
42 | "react-dom": "^18.2.0",
43 | "react-dropzone": "^14.3.8",
44 | "react-error-boundary": "^4.0.13",
45 | "react-hook-form": "7.49.3",
46 | "react-icons": "5.0.1",
47 | "react-resizable-panels": "^2.1.7",
48 | "react-syntax-highlighter": "^15.6.1",
49 | "tailwind-merge": "^3.0.2",
50 | "tailwindcss-animate": "^1.0.7",
51 | "tw-animate-css": "^1.2.5"
52 | },
53 | "devDependencies": {
54 | "@biomejs/biome": "1.6.1",
55 | "@hey-api/openapi-ts": "^0.34.1",
56 | "@playwright/test": "^1.45.2",
57 | "@shadcn/ui": "^0.0.4",
58 | "@tailwindcss/forms": "^0.5.10",
59 | "@tanstack/router-devtools": "^1.19.1",
60 | "@tanstack/router-vite-plugin": "^1.19.0",
61 | "@types/node": "^20.17.28",
62 | "@types/react": "^18.2.37",
63 | "@types/react-dom": "^18.2.15",
64 | "@vitejs/plugin-react-swc": "^3.5.0",
65 | "autoprefixer": "^10.4.21",
66 | "dotenv": "^16.4.5",
67 | "postcss": "^8.5.3",
68 | "postcss-import": "^16.1.0",
69 | "postcss-preset-env": "^10.1.5",
70 | "tailwindcss": "^4.0.17",
71 | "typescript": "^5.2.2",
72 | "vite": "^5.0.13"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/frontend/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, devices } from '@playwright/test';
2 |
3 |
4 | /**
5 | * Read environment variables from file.
6 | * https://github.com/motdotla/dotenv
7 | */
8 | // require('dotenv').config();
9 |
10 | /**
11 | * See https://playwright.dev/docs/test-configuration.
12 | */
13 | export default defineConfig({
14 | testDir: './tests',
15 | /* Run tests in files in parallel */
16 | fullyParallel: true,
17 | /* Fail the build on CI if you accidentally left test.only in the source code. */
18 | forbidOnly: !!process.env.CI,
19 | /* Retry on CI only */
20 | retries: process.env.CI ? 2 : 0,
21 | /* Opt out of parallel tests on CI. */
22 | workers: process.env.CI ? 1 : undefined,
23 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */
24 | reporter: 'html',
25 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
26 | use: {
27 | /* Base URL to use in actions like `await page.goto('/')`. */
28 | baseURL: 'http://localhost:5173',
29 |
30 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
31 | trace: 'on-first-retry',
32 | },
33 |
34 | /* Configure projects for major browsers */
35 | projects: [
36 | { name: 'setup', testMatch: /.*\.setup\.ts/ },
37 |
38 | {
39 | name: 'chromium',
40 | use: {
41 | ...devices['Desktop Chrome'],
42 | storageState: 'playwright/.auth/user.json',
43 | },
44 | dependencies: ['setup'],
45 | },
46 |
47 | // {
48 | // name: 'firefox',
49 | // use: {
50 | // ...devices['Desktop Firefox'],
51 | // storageState: 'playwright/.auth/user.json',
52 | // },
53 | // dependencies: ['setup'],
54 | // },
55 |
56 | // {
57 | // name: 'webkit',
58 | // use: {
59 | // ...devices['Desktop Safari'],
60 | // storageState: 'playwright/.auth/user.json',
61 | // },
62 | // dependencies: ['setup'],
63 | // },
64 |
65 | /* Test against mobile viewports. */
66 | // {
67 | // name: 'Mobile Chrome',
68 | // use: { ...devices['Pixel 5'] },
69 | // },
70 | // {
71 | // name: 'Mobile Safari',
72 | // use: { ...devices['iPhone 12'] },
73 | // },
74 |
75 | /* Test against branded browsers. */
76 | // {
77 | // name: 'Microsoft Edge',
78 | // use: { ...devices['Desktop Edge'], channel: 'msedge' },
79 | // },
80 | // {
81 | // name: 'Google Chrome',
82 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
83 | // },
84 | ],
85 |
86 | /* Run your local dev server before starting the tests */
87 | webServer: {
88 | command: 'npm run dev',
89 | url: 'http://localhost:5173',
90 | reuseExistingServer: !process.env.CI,
91 | },
92 | });
93 |
--------------------------------------------------------------------------------
/frontend/playwright/.auth/user.json:
--------------------------------------------------------------------------------
1 | {
2 | "cookies": [],
3 | "origins": []
4 | }
--------------------------------------------------------------------------------
/frontend/public/assets/images/camel-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
67 |
--------------------------------------------------------------------------------
/frontend/public/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/camel-ai/camel_web_app/896153609faf66b6e3f8040ca109b4993ac0783c/frontend/public/assets/images/favicon.png
--------------------------------------------------------------------------------
/frontend/src/App.css:
--------------------------------------------------------------------------------
1 | #root {
2 | /* max-width: 1280px; */
3 | /* margin: 0 auto; */
4 | /* padding: 2rem; */
5 | /* text-align: center; */
6 | }
7 |
8 | .logo {
9 | height: 6em;
10 | padding: 1.5em;
11 | will-change: filter;
12 | transition: filter 300ms;
13 | }
14 | .logo:hover {
15 | filter: drop-shadow(0 0 2em #646cffaa);
16 | }
17 | .logo.react:hover {
18 | filter: drop-shadow(0 0 2em #61dafbaa);
19 | }
20 |
21 | @keyframes logo-spin {
22 | from {
23 | transform: rotate(0deg);
24 | }
25 | to {
26 | transform: rotate(360deg);
27 | }
28 | }
29 |
30 | @media (prefers-reduced-motion: no-preference) {
31 | a:nth-of-type(2) .logo {
32 | animation: logo-spin infinite 20s linear;
33 | }
34 | }
35 |
36 | .card {
37 | padding: 2em;
38 | }
39 |
40 | .read-the-docs {
41 | color: #888;
42 | }
43 |
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import AIPlayground from './AIPlayground'
3 | import './AIPlayground.css'
4 | import './App.css'
5 |
6 | function App() {
7 | const [count, setCount] = useState(0)
8 | return (
9 |
10 |
13 | );
14 | }
15 |
16 | export default App
17 |
--------------------------------------------------------------------------------
/frontend/src/Canvas.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface CanvasProps {
4 | children: React.ReactNode;
5 | }
6 |
7 | const Canvas: React.FC
= ({ children }) => {
8 | return (
9 |
10 | {/* 网格背景 */}
11 |
12 | {/* 内容区域 */}
13 |
14 | {children}
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default Canvas;
--------------------------------------------------------------------------------
/frontend/src/assets/fonts/OpenSans.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/camel-ai/camel_web_app/896153609faf66b6e3f8040ca109b4993ac0783c/frontend/src/assets/fonts/OpenSans.ttf
--------------------------------------------------------------------------------
/frontend/src/assets/fonts/Palatino.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/camel-ai/camel_web_app/896153609faf66b6e3f8040ca109b4993ac0783c/frontend/src/assets/fonts/Palatino.ttf
--------------------------------------------------------------------------------
/frontend/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/canvas.css:
--------------------------------------------------------------------------------
1 | .canvas-container {
2 | width: 100%;
3 | height: 100vh;
4 | background: #f5f5f5;
5 | position: relative;
6 | overflow: hidden;
7 | }
8 |
9 | .canvas-grid {
10 | width: 100%;
11 | height: 100%;
12 | background-image: radial-gradient(circle, #e0e0e0 1px, transparent 1px);
13 | background-size: 20px 20px;
14 | }
15 |
16 | .canvas-content {
17 | position: absolute;
18 | top: 0;
19 | left: 0;
20 | width: 100%;
21 | height: 100%;
22 | }
--------------------------------------------------------------------------------
/frontend/src/components/app-sidebar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import {
3 | AudioWaveform,
4 | BookOpen,
5 | Bot,
6 | Command,
7 | Frame,
8 | GalleryVerticalEnd,
9 | Map,
10 | PieChart,
11 | Settings2,
12 | SquareTerminal,
13 | Github,
14 | BookText,
15 | } from "lucide-react"
16 |
17 | import { NavMain } from "@/components/nav-main"
18 | import { NavProjects } from "@/components/nav-projects"
19 | import { NavUser } from "@/components/nav-user"
20 | import { TeamSwitcher } from "@/components/team-switcher"
21 | import {
22 | Sidebar,
23 | SidebarContent,
24 | SidebarFooter,
25 | SidebarHeader,
26 | SidebarRail,
27 | } from "@/components/ui/sidebar"
28 |
29 | // This is sample data.
30 | const data = {
31 | user: {
32 | name: "camel",
33 | email: "m@example.com",
34 | avatar: "/avatars/shadcn.jpg",
35 | },
36 | teams: [
37 | {
38 | name: "Acme Inc",
39 | logo: GalleryVerticalEnd,
40 | plan: "Enterprise",
41 | },
42 | {
43 | name: "Acme Corp.",
44 | logo: AudioWaveform,
45 | plan: "Startup",
46 | },
47 | {
48 | name: "Evil Corp.",
49 | logo: Command,
50 | plan: "Free",
51 | },
52 | ],
53 | navMain: [
54 | {
55 | title: "Playground",
56 | url: "#",
57 | icon: SquareTerminal,
58 | isActive: true,
59 | items: [
60 | {
61 | title: "Create Your First Agent",
62 | url: "#",
63 | },
64 | {
65 | title: "Role Playing Session",
66 | url: "#",
67 | },
68 | {
69 | title: "Workforce Session",
70 | url: "#",
71 | },
72 | {
73 | title: "Synthetic Data",
74 | url: "#",
75 | },
76 | {
77 | title: "RAG&Graph RAG",
78 | url: "#",
79 | },
80 | {
81 | title: "Human-in-the-loop",
82 | url: "#",
83 | },
84 | ],
85 | },
86 | // {
87 | // title: "Models",
88 | // url: "#",
89 | // icon: Bot,
90 | // items: [
91 | // {
92 | // title: "Genesis",
93 | // url: "#",
94 | // },
95 | // {
96 | // title: "Explorer",
97 | // url: "#",
98 | // },
99 | // {
100 | // title: "Quantum",
101 | // url: "#",
102 | // },
103 | // ],
104 | // },
105 | // {
106 | // title: "Documentation",
107 | // url: "#",
108 | // icon: BookOpen,
109 | // items: [
110 | // {
111 | // title: "Introduction",
112 | // url: "#",
113 | // },
114 | // {
115 | // title: "Get Started",
116 | // url: "#",
117 | // },
118 | // {
119 | // title: "Tutorials",
120 | // url: "#",
121 | // },
122 | // {
123 | // title: "Changelog",
124 | // url: "#",
125 | // },
126 | // ],
127 | // },
128 | // {
129 | // title: "Settings",
130 | // url: "#",
131 | // icon: Settings2,
132 | // items: [
133 | // {
134 | // title: "General",
135 | // url: "#",
136 | // },
137 | // {
138 | // title: "Team",
139 | // url: "#",
140 | // },
141 | // {
142 | // title: "Billing",
143 | // url: "#",
144 | // },
145 | // {
146 | // title: "Limits",
147 | // url: "#",
148 | // },
149 | // ],
150 | // },
151 | ],
152 | projects: [
153 | // {
154 | // name: "Design Engineering",
155 | // url: "#",
156 | // icon: Frame,
157 | // },
158 | // {
159 | // name: "Sales & Marketing",
160 | // url: "#",
161 | // icon: PieChart,
162 | // },
163 | // {
164 | // name: "Travel",
165 | // url: "#",
166 | // icon: Map,
167 | // },
168 | ],
169 | }
170 |
171 | export interface AppSidebarProps extends React.ComponentProps {
172 | onModuleChange?: (moduleId: string) => void;
173 | }
174 |
175 | const moduleIdMap = {
176 | "Create Your First Agent": "Module1",
177 | "Role Playing Session": "Module2",
178 | "Workforce Session": "Module3",
179 | "Synthetic Data": "Module4",
180 | "RAG&Graph RAG": "Module5",
181 | "Human-in-the-loop": "Module6"
182 | };
183 |
184 | export function AppSidebar({ onModuleChange, ...props }: AppSidebarProps) {
185 | const [starCount, setStarCount] = React.useState(0);
186 |
187 | React.useEffect(() => {
188 | fetch('https://api.github.com/repos/camel-ai/camel')
189 | .then(response => response.json())
190 | .then(data => setStarCount(data.stargazers_count))
191 | .catch(error => console.error('Error fetching star count:', error));
192 | }, []);
193 |
194 | const handleItemClick = (title: string) => {
195 | const moduleId = moduleIdMap[title];
196 | if (moduleId && onModuleChange) {
197 | onModuleChange(moduleId);
198 | }
199 | };
200 |
201 | const navMainWithHandlers = data.navMain.map(section => ({
202 | ...section,
203 | items: section.items?.map(item => ({
204 | ...item,
205 | onClick: () => handleItemClick(item.title)
206 | }))
207 | }));
208 |
209 | return (
210 |
211 |
212 |
213 |

214 |
215 | {/* */}
216 |
217 |
218 |
219 |
220 |
221 |
222 |
242 | {/* */}
243 |
244 |
245 |
246 | )
247 | }
248 |
--------------------------------------------------------------------------------
/frontend/src/components/nav-main.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { ChevronRight, type LucideIcon } from "lucide-react"
4 |
5 | import {
6 | Collapsible,
7 | CollapsibleContent,
8 | CollapsibleTrigger,
9 | } from "@/components/ui/collapsible"
10 | import {
11 | SidebarGroup,
12 | SidebarGroupLabel,
13 | SidebarMenu,
14 | SidebarMenuButton,
15 | SidebarMenuItem,
16 | SidebarMenuSub,
17 | SidebarMenuSubButton,
18 | SidebarMenuSubItem,
19 | } from "@/components/ui/sidebar"
20 |
21 | export function NavMain({
22 | items,
23 | }: {
24 | items: {
25 | title: string
26 | url: string
27 | icon?: LucideIcon
28 | isActive?: boolean
29 | items?: {
30 | title: string
31 | url: string
32 | onClick?: () => void
33 | }[]
34 | }[]
35 | }) {
36 | return (
37 |
38 | CAMEL
39 |
40 | {items.map((item) => (
41 |
47 |
48 |
49 |
50 | {item.icon && }
51 | {item.title}
52 |
53 |
54 |
55 |
56 |
57 | {item.items?.map((subItem) => (
58 |
59 |
63 | {subItem.title}
64 |
65 |
66 | ))}
67 |
68 |
69 |
70 |
71 | ))}
72 |
73 |
74 | )
75 | }
76 |
--------------------------------------------------------------------------------
/frontend/src/components/nav-projects.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Folder,
3 | Forward,
4 | MoreHorizontal,
5 | Trash2,
6 | type LucideIcon,
7 | } from "lucide-react"
8 |
9 | import {
10 | DropdownMenu,
11 | DropdownMenuContent,
12 | DropdownMenuItem,
13 | DropdownMenuSeparator,
14 | DropdownMenuTrigger,
15 | } from "@/components/ui/dropdown-menu"
16 | import {
17 | SidebarGroup,
18 | SidebarGroupLabel,
19 | SidebarMenu,
20 | SidebarMenuAction,
21 | SidebarMenuButton,
22 | SidebarMenuItem,
23 | useSidebar,
24 | } from "@/components/ui/sidebar"
25 |
26 | export function NavProjects({
27 | projects,
28 | }: {
29 | projects: {
30 | name: string
31 | url: string
32 | icon: LucideIcon
33 | }[]
34 | }) {
35 | const { isMobile } = useSidebar()
36 |
37 | return (
38 |
39 | {/* Projects */}
40 |
41 | {projects.map((item) => (
42 |
43 |
44 |
45 |
46 | {item.name}
47 |
48 |
49 |
50 |
51 |
52 |
53 | More
54 |
55 |
56 |
61 |
62 |
63 | View Project
64 |
65 |
66 |
67 | Share Project
68 |
69 |
70 |
71 |
72 | Delete Project
73 |
74 |
75 |
76 |
77 | ))}
78 | {/*
79 |
80 |
81 | More
82 |
83 | */}
84 |
85 |
86 | )
87 | }
88 |
--------------------------------------------------------------------------------
/frontend/src/components/nav-user.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | BadgeCheck,
5 | Bell,
6 | ChevronsUpDown,
7 | CreditCard,
8 | LogOut,
9 | Sparkles,
10 | } from "lucide-react"
11 |
12 | import {
13 | Avatar,
14 | AvatarFallback,
15 | AvatarImage,
16 | } from "@/components/ui/avatar"
17 | import {
18 | DropdownMenu,
19 | DropdownMenuContent,
20 | DropdownMenuGroup,
21 | DropdownMenuItem,
22 | DropdownMenuLabel,
23 | DropdownMenuSeparator,
24 | DropdownMenuTrigger,
25 | } from "@/components/ui/dropdown-menu"
26 | import {
27 | SidebarMenu,
28 | SidebarMenuButton,
29 | SidebarMenuItem,
30 | useSidebar,
31 | } from "@/components/ui/sidebar"
32 |
33 | export function NavUser({
34 | user,
35 | }: {
36 | user: {
37 | name: string
38 | email: string
39 | avatar: string
40 | }
41 | }) {
42 | const { isMobile } = useSidebar()
43 |
44 | return (
45 |
46 |
47 |
48 |
49 |
53 |
54 |
55 | CN
56 |
57 |
58 | {user.name}
59 | {user.email}
60 |
61 |
62 |
63 |
64 |
70 |
71 |
72 |
73 |
74 | CN
75 |
76 |
77 | {user.name}
78 | {user.email}
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | Upgrade to Pro
87 |
88 |
89 |
90 |
91 |
92 |
93 | Account
94 |
95 |
96 |
97 | Billing
98 |
99 |
100 |
101 | Notifications
102 |
103 |
104 |
105 |
106 |
107 | Log out
108 |
109 |
110 |
111 |
112 |
113 | )
114 | }
115 |
--------------------------------------------------------------------------------
/frontend/src/components/team-switcher.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { ChevronsUpDown, Plus } from "lucide-react"
3 |
4 | import {
5 | DropdownMenu,
6 | DropdownMenuContent,
7 | DropdownMenuItem,
8 | DropdownMenuLabel,
9 | DropdownMenuSeparator,
10 | DropdownMenuShortcut,
11 | DropdownMenuTrigger,
12 | } from "@/components/ui/dropdown-menu"
13 | import {
14 | SidebarMenu,
15 | SidebarMenuButton,
16 | SidebarMenuItem,
17 | useSidebar,
18 | } from "@/components/ui/sidebar"
19 |
20 | export function TeamSwitcher({
21 | teams,
22 | }: {
23 | teams: {
24 | name: string
25 | logo: React.ElementType
26 | plan: string
27 | }[]
28 | }) {
29 | const { isMobile } = useSidebar()
30 | const [activeTeam, setActiveTeam] = React.useState(teams[0])
31 |
32 | if (!activeTeam) {
33 | return null
34 | }
35 |
36 | return (
37 |
38 |
39 |
40 |
41 |
45 |
48 |
49 | {activeTeam.name}
50 | {activeTeam.plan}
51 |
52 |
53 |
54 |
55 |
61 |
62 | Teams
63 |
64 | {teams.map((team, index) => (
65 | setActiveTeam(team)}
68 | className="gap-2 p-2"
69 | >
70 |
71 |
72 |
73 | {team.name}
74 | ⌘{index + 1}
75 |
76 | ))}
77 |
78 |
79 |
82 | Add team
83 |
84 |
85 |
86 |
87 |
88 | )
89 | }
90 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/library/utils"
7 |
8 | function Avatar({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
21 | )
22 | }
23 |
24 | function AvatarImage({
25 | className,
26 | ...props
27 | }: React.ComponentProps) {
28 | return (
29 |
34 | )
35 | }
36 |
37 | function AvatarFallback({
38 | className,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
50 | )
51 | }
52 |
53 | export { Avatar, AvatarImage, AvatarFallback }
54 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { ChevronRight, MoreHorizontal } from "lucide-react"
4 |
5 | import { cn } from "@/library/utils"
6 |
7 | function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
8 | return
9 | }
10 |
11 | function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
12 | return (
13 |
21 | )
22 | }
23 |
24 | function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
25 | return (
26 |
31 | )
32 | }
33 |
34 | function BreadcrumbLink({
35 | asChild,
36 | className,
37 | ...props
38 | }: React.ComponentProps<"a"> & {
39 | asChild?: boolean
40 | }) {
41 | const Comp = asChild ? Slot : "a"
42 |
43 | return (
44 |
49 | )
50 | }
51 |
52 | function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
53 | return (
54 |
62 | )
63 | }
64 |
65 | function BreadcrumbSeparator({
66 | children,
67 | className,
68 | ...props
69 | }: React.ComponentProps<"li">) {
70 | return (
71 | svg]:size-3.5", className)}
76 | {...props}
77 | >
78 | {children ?? }
79 |
80 | )
81 | }
82 |
83 | function BreadcrumbEllipsis({
84 | className,
85 | ...props
86 | }: React.ComponentProps<"span">) {
87 | return (
88 |
95 |
96 | More
97 |
98 | )
99 | }
100 |
101 | export {
102 | Breadcrumb,
103 | BreadcrumbList,
104 | BreadcrumbItem,
105 | BreadcrumbLink,
106 | BreadcrumbPage,
107 | BreadcrumbSeparator,
108 | BreadcrumbEllipsis,
109 | }
110 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/library/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16 | outline:
17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 | ghost:
21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22 | link: "text-primary underline-offset-4 hover:underline",
23 | },
24 | size: {
25 | default: "h-9 px-4 py-2 has-[>svg]:px-3",
26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28 | icon: "size-9",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default",
34 | },
35 | }
36 | )
37 |
38 | function Button({
39 | className,
40 | variant,
41 | size,
42 | asChild = false,
43 | ...props
44 | }: React.ComponentProps<"button"> &
45 | VariantProps & {
46 | asChild?: boolean
47 | }) {
48 | const Comp = asChild ? Slot : "button"
49 |
50 | return (
51 |
56 | )
57 | }
58 |
59 | export { Button, buttonVariants }
60 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/library/utils"
4 |
5 | function Card({ className, ...props }: React.ComponentProps<"div">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19 | return (
20 |
25 | )
26 | }
27 |
28 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
29 | return (
30 |
35 | )
36 | }
37 |
38 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
39 | return (
40 |
45 | )
46 | }
47 |
48 | function CardContent({ className, ...props }: React.ComponentProps<"div">) {
49 | return (
50 |
55 | )
56 | }
57 |
58 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
59 | return (
60 |
65 | )
66 | }
67 |
68 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
69 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/chat-input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cn } from "@/library/utils"
3 |
4 | export interface ChatInputProps
5 | extends React.TextareaHTMLAttributes {}
6 |
7 | const ChatInput = React.forwardRef(
8 | ({ className, ...props }, ref) => {
9 | return (
10 |
19 | )
20 | }
21 | )
22 | ChatInput.displayName = "ChatInput"
23 |
24 | export { ChatInput }
--------------------------------------------------------------------------------
/frontend/src/components/ui/chat/chat-bubble.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 | import { cn } from "@/library/utils";
4 | import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
5 | import MessageLoading from "./message-loading";
6 | import { Button, ButtonProps } from "../button";
7 |
8 | // ChatBubble
9 | const chatBubbleVariant = cva(
10 | "flex gap-2 max-w-[60%] items-end relative group",
11 | {
12 | variants: {
13 | variant: {
14 | received: "self-start",
15 | sent: "self-end flex-row-reverse",
16 | },
17 | layout: {
18 | default: "",
19 | ai: "max-w-full w-full items-center",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "received",
24 | layout: "default",
25 | },
26 | },
27 | );
28 |
29 | interface ChatBubbleProps
30 | extends React.HTMLAttributes,
31 | VariantProps {}
32 |
33 | const ChatBubble = React.forwardRef(
34 | ({ className, variant, layout, children, ...props }, ref) => (
35 |
43 | {React.Children.map(children, (child) =>
44 | React.isValidElement(child) && typeof child.type !== "string"
45 | ? React.cloneElement(child, {
46 | variant,
47 | layout,
48 | } as React.ComponentProps)
49 | : child,
50 | )}
51 |
52 | ),
53 | );
54 | ChatBubble.displayName = "ChatBubble";
55 |
56 | // ChatBubbleAvatar
57 | interface ChatBubbleAvatarProps {
58 | src?: string;
59 | fallback?: string;
60 | className?: string;
61 | }
62 |
63 | const ChatBubbleAvatar: React.FC = ({
64 | src,
65 | fallback,
66 | className,
67 | }) => (
68 |
69 |
70 | {fallback}
71 |
72 | );
73 |
74 | // ChatBubbleMessage
75 | const chatBubbleMessageVariants = cva("p-4", {
76 | variants: {
77 | variant: {
78 | received:
79 | "bg-secondary text-secondary-foreground rounded-r-lg rounded-tl-lg",
80 | sent: "bg-primary text-primary-foreground rounded-l-lg rounded-tr-lg",
81 | },
82 | layout: {
83 | default: "",
84 | ai: "border-t w-full rounded-none bg-transparent",
85 | },
86 | },
87 | defaultVariants: {
88 | variant: "received",
89 | layout: "default",
90 | },
91 | });
92 |
93 | interface ChatBubbleMessageProps
94 | extends React.HTMLAttributes,
95 | VariantProps {
96 | isLoading?: boolean;
97 | }
98 |
99 | const ChatBubbleMessage = React.forwardRef<
100 | HTMLDivElement,
101 | ChatBubbleMessageProps
102 | >(
103 | (
104 | { className, variant, layout, isLoading = false, children, ...props },
105 | ref,
106 | ) => (
107 |
115 | {isLoading ? (
116 |
117 |
118 |
119 | ) : (
120 | children
121 | )}
122 |
123 | ),
124 | );
125 | ChatBubbleMessage.displayName = "ChatBubbleMessage";
126 |
127 | // ChatBubbleTimestamp
128 | interface ChatBubbleTimestampProps
129 | extends React.HTMLAttributes {
130 | timestamp: string;
131 | }
132 |
133 | const ChatBubbleTimestamp: React.FC = ({
134 | timestamp,
135 | className,
136 | ...props
137 | }) => (
138 |
139 | {timestamp}
140 |
141 | );
142 |
143 | // ChatBubbleAction
144 | type ChatBubbleActionProps = ButtonProps & {
145 | icon: React.ReactNode;
146 | };
147 |
148 | const ChatBubbleAction: React.FC = ({
149 | icon,
150 | onClick,
151 | className,
152 | variant = "ghost",
153 | size = "icon",
154 | ...props
155 | }) => (
156 |
165 | );
166 |
167 | interface ChatBubbleActionWrapperProps
168 | extends React.HTMLAttributes {
169 | variant?: "sent" | "received";
170 | className?: string;
171 | }
172 |
173 | const ChatBubbleActionWrapper = React.forwardRef<
174 | HTMLDivElement,
175 | ChatBubbleActionWrapperProps
176 | >(({ variant, className, children, ...props }, ref) => (
177 |
188 | {children}
189 |
190 | ));
191 | ChatBubbleActionWrapper.displayName = "ChatBubbleActionWrapper";
192 |
193 | export {
194 | ChatBubble,
195 | ChatBubbleAvatar,
196 | ChatBubbleMessage,
197 | ChatBubbleTimestamp,
198 | chatBubbleVariant,
199 | chatBubbleMessageVariants,
200 | ChatBubbleAction,
201 | ChatBubbleActionWrapper,
202 | };
203 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/chat/chat-input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Textarea } from "@/components/ui/textarea";
3 | import { cn } from "@/library/utils";
4 |
5 | interface ChatInputProps extends React.TextareaHTMLAttributes{}
6 |
7 | const ChatInput = React.forwardRef(
8 | ({ className, ...props }, ref) => (
9 |
19 | ),
20 | );
21 | ChatInput.displayName = "ChatInput";
22 |
23 | export { ChatInput };
24 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/chat/chat-message-list.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { ArrowDown } from "lucide-react";
3 | import { Button } from "@/components/ui/button";
4 | import { useAutoScroll } from "@/components/ui/chat/hooks/useAutoScroll";
5 | import { cn } from "@/library/utils";
6 |
7 | interface ChatMessageListProps extends React.HTMLAttributes {
8 | smooth?: boolean;
9 | }
10 |
11 | const ChatMessageList = React.forwardRef(
12 | ({ className, children, smooth = false, ...props }, ref) => {
13 | const {
14 | scrollRef,
15 | isAtBottom,
16 | autoScrollEnabled,
17 | scrollToBottom,
18 | disableAutoScroll,
19 | } = useAutoScroll({
20 | smooth,
21 | content: children,
22 | });
23 |
24 | return (
25 |
26 |
31 | {children}
32 |
33 |
34 | {!isAtBottom && (
35 |
46 | )}
47 |
48 | );
49 | }
50 | );
51 |
52 | ChatMessageList.displayName = "ChatMessageList";
53 |
54 | export { ChatMessageList };
55 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/chat/expandable-chat.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useRef, useState } from "react";
4 | import { X, MessageCircle } from "lucide-react";
5 | import { cn } from "@/library/utils";
6 | import { Button } from "@/components/ui/button";
7 |
8 | export type ChatPosition = "bottom-right" | "bottom-left";
9 | export type ChatSize = "sm" | "md" | "lg" | "xl" | "full";
10 |
11 | const chatConfig = {
12 | dimensions: {
13 | sm: "sm:max-w-sm sm:max-h-[500px]",
14 | md: "sm:max-w-md sm:max-h-[600px]",
15 | lg: "sm:max-w-lg sm:max-h-[700px]",
16 | xl: "sm:max-w-xl sm:max-h-[800px]",
17 | full: "sm:w-full sm:h-full",
18 | },
19 | positions: {
20 | "bottom-right": "bottom-5 right-5",
21 | "bottom-left": "bottom-5 left-5",
22 | },
23 | chatPositions: {
24 | "bottom-right": "sm:bottom-[calc(100%+10px)] sm:right-0",
25 | "bottom-left": "sm:bottom-[calc(100%+10px)] sm:left-0",
26 | },
27 | states: {
28 | open: "pointer-events-auto opacity-100 visible scale-100 translate-y-0",
29 | closed:
30 | "pointer-events-none opacity-0 invisible scale-100 sm:translate-y-5",
31 | },
32 | };
33 |
34 | interface ExpandableChatProps extends React.HTMLAttributes {
35 | position?: ChatPosition;
36 | size?: ChatSize;
37 | icon?: React.ReactNode;
38 | }
39 |
40 | const ExpandableChat: React.FC = ({
41 | className,
42 | position = "bottom-right",
43 | size = "md",
44 | icon,
45 | children,
46 | ...props
47 | }) => {
48 | const [isOpen, setIsOpen] = useState(false);
49 | const chatRef = useRef(null);
50 |
51 | const toggleChat = () => setIsOpen(!isOpen);
52 |
53 | return (
54 |
58 |
68 | {children}
69 |
77 |
78 |
83 |
84 | );
85 | };
86 |
87 | ExpandableChat.displayName = "ExpandableChat";
88 |
89 | const ExpandableChatHeader: React.FC> = ({
90 | className,
91 | ...props
92 | }) => (
93 |
97 | );
98 |
99 | ExpandableChatHeader.displayName = "ExpandableChatHeader";
100 |
101 | const ExpandableChatBody: React.FC> = ({
102 | className,
103 | ...props
104 | }) => ;
105 |
106 | ExpandableChatBody.displayName = "ExpandableChatBody";
107 |
108 | const ExpandableChatFooter: React.FC> = ({
109 | className,
110 | ...props
111 | }) => ;
112 |
113 | ExpandableChatFooter.displayName = "ExpandableChatFooter";
114 |
115 | interface ExpandableChatToggleProps
116 | extends React.ButtonHTMLAttributes {
117 | icon?: React.ReactNode;
118 | isOpen: boolean;
119 | toggleChat: () => void;
120 | }
121 |
122 | const ExpandableChatToggle: React.FC = ({
123 | className,
124 | icon,
125 | isOpen,
126 | toggleChat,
127 | ...props
128 | }) => (
129 |
144 | );
145 |
146 | ExpandableChatToggle.displayName = "ExpandableChatToggle";
147 |
148 | export {
149 | ExpandableChat,
150 | ExpandableChatHeader,
151 | ExpandableChatBody,
152 | ExpandableChatFooter,
153 | };
154 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/chat/hooks/useAutoScroll.tsx:
--------------------------------------------------------------------------------
1 | // @hidden
2 | import { useCallback, useEffect, useRef, useState } from "react";
3 |
4 | interface ScrollState {
5 | isAtBottom: boolean;
6 | autoScrollEnabled: boolean;
7 | }
8 |
9 | interface UseAutoScrollOptions {
10 | offset?: number;
11 | smooth?: boolean;
12 | content?: React.ReactNode;
13 | }
14 |
15 | export function useAutoScroll(options: UseAutoScrollOptions = {}) {
16 | const { offset = 20, smooth = false, content } = options;
17 | const scrollRef = useRef(null);
18 | const lastContentHeight = useRef(0);
19 | const userHasScrolled = useRef(false);
20 |
21 | const [scrollState, setScrollState] = useState({
22 | isAtBottom: true,
23 | autoScrollEnabled: true,
24 | });
25 |
26 | const checkIsAtBottom = useCallback(
27 | (element: HTMLElement) => {
28 | const { scrollTop, scrollHeight, clientHeight } = element;
29 | const distanceToBottom = Math.abs(
30 | scrollHeight - scrollTop - clientHeight
31 | );
32 | return distanceToBottom <= offset;
33 | },
34 | [offset]
35 | );
36 |
37 | const scrollToBottom = useCallback(
38 | (instant?: boolean) => {
39 | if (!scrollRef.current) return;
40 |
41 | const targetScrollTop =
42 | scrollRef.current.scrollHeight - scrollRef.current.clientHeight;
43 |
44 | if (instant) {
45 | scrollRef.current.scrollTop = targetScrollTop;
46 | } else {
47 | scrollRef.current.scrollTo({
48 | top: targetScrollTop,
49 | behavior: smooth ? "smooth" : "auto",
50 | });
51 | }
52 |
53 | setScrollState({
54 | isAtBottom: true,
55 | autoScrollEnabled: true,
56 | });
57 | userHasScrolled.current = false;
58 | },
59 | [smooth]
60 | );
61 |
62 | const handleScroll = useCallback(() => {
63 | if (!scrollRef.current) return;
64 |
65 | const atBottom = checkIsAtBottom(scrollRef.current);
66 |
67 | setScrollState((prev) => ({
68 | isAtBottom: atBottom,
69 | // Re-enable auto-scroll if at the bottom
70 | autoScrollEnabled: atBottom ? true : prev.autoScrollEnabled,
71 | }));
72 | }, [checkIsAtBottom]);
73 |
74 | useEffect(() => {
75 | const element = scrollRef.current;
76 | if (!element) return;
77 |
78 | element.addEventListener("scroll", handleScroll, { passive: true });
79 | return () => element.removeEventListener("scroll", handleScroll);
80 | }, [handleScroll]);
81 |
82 | useEffect(() => {
83 | const scrollElement = scrollRef.current;
84 | if (!scrollElement) return;
85 |
86 | const currentHeight = scrollElement.scrollHeight;
87 | const hasNewContent = currentHeight !== lastContentHeight.current;
88 |
89 | if (hasNewContent) {
90 | if (scrollState.autoScrollEnabled) {
91 | requestAnimationFrame(() => {
92 | scrollToBottom(lastContentHeight.current === 0);
93 | });
94 | }
95 | lastContentHeight.current = currentHeight;
96 | }
97 | }, [content, scrollState.autoScrollEnabled, scrollToBottom]);
98 |
99 | useEffect(() => {
100 | const element = scrollRef.current;
101 | if (!element) return;
102 |
103 | const resizeObserver = new ResizeObserver(() => {
104 | if (scrollState.autoScrollEnabled) {
105 | scrollToBottom(true);
106 | }
107 | });
108 |
109 | resizeObserver.observe(element);
110 | return () => resizeObserver.disconnect();
111 | }, [scrollState.autoScrollEnabled, scrollToBottom]);
112 |
113 | const disableAutoScroll = useCallback(() => {
114 | const atBottom = scrollRef.current
115 | ? checkIsAtBottom(scrollRef.current)
116 | : false;
117 |
118 | // Only disable if not at bottom
119 | if (!atBottom) {
120 | userHasScrolled.current = true;
121 | setScrollState((prev) => ({
122 | ...prev,
123 | autoScrollEnabled: false,
124 | }));
125 | }
126 | }, [checkIsAtBottom]);
127 |
128 | return {
129 | scrollRef,
130 | isAtBottom: scrollState.isAtBottom,
131 | autoScrollEnabled: scrollState.autoScrollEnabled,
132 | scrollToBottom: () => scrollToBottom(false),
133 | disableAutoScroll,
134 | };
135 | }
136 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/chat/message-loading.tsx:
--------------------------------------------------------------------------------
1 | // @hidden
2 | export default function MessageLoading() {
3 | return (
4 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
3 | import { CheckIcon } from "lucide-react"
4 |
5 | import { cn } from "@/library/utils"
6 |
7 | function Checkbox({
8 | className,
9 | ...props
10 | }: React.ComponentProps) {
11 | return (
12 |
20 |
24 |
25 |
26 |
27 | )
28 | }
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/code-block.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from "react";
3 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
4 | import { atomDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
5 | import { IconCheck, IconCopy } from "@tabler/icons-react";
6 |
7 | type CodeBlockProps = {
8 | language: string;
9 | filename: string;
10 | highlightLines?: number[];
11 | } & (
12 | | {
13 | code: string;
14 | tabs?: never;
15 | }
16 | | {
17 | code?: never;
18 | tabs: Array<{
19 | name: string;
20 | code: string;
21 | language?: string;
22 | highlightLines?: number[];
23 | }>;
24 | }
25 | );
26 |
27 | export const CodeBlock = ({
28 | language,
29 | filename,
30 | code,
31 | highlightLines = [],
32 | tabs = [],
33 | }: CodeBlockProps) => {
34 | const [copied, setCopied] = React.useState(false);
35 | const [activeTab, setActiveTab] = React.useState(0);
36 |
37 | const tabsExist = tabs.length > 0;
38 |
39 | const copyToClipboard = async () => {
40 | const textToCopy = tabsExist ? tabs[activeTab].code : code;
41 | if (textToCopy) {
42 | await navigator.clipboard.writeText(textToCopy);
43 | setCopied(true);
44 | setTimeout(() => setCopied(false), 2000);
45 | }
46 | };
47 |
48 | const activeCode = tabsExist ? tabs[activeTab].code : code;
49 | const activeLanguage = tabsExist
50 | ? tabs[activeTab].language || language
51 | : language;
52 | const activeHighlightLines = tabsExist
53 | ? tabs[activeTab].highlightLines || []
54 | : highlightLines;
55 |
56 | return (
57 |
58 |
59 | {tabsExist && (
60 |
61 | {tabs.map((tab, index) => (
62 |
73 | ))}
74 |
75 | )}
76 | {!tabsExist && filename && (
77 |
78 |
{filename}
79 |
85 |
86 | )}
87 |
88 |
({
100 | style: {
101 | backgroundColor: activeHighlightLines.includes(lineNumber)
102 | ? "rgba(255,255,255,0.1)"
103 | : "transparent",
104 | display: "block",
105 | width: "100%",
106 | },
107 | })}
108 | PreTag="div"
109 | >
110 | {String(activeCode)}
111 |
112 |
113 | );
114 | };
115 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/code-editor.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cn } from "@/library/utils"
3 |
4 | interface CodeEditorProps {
5 | language: string
6 | filename?: string
7 | code: string
8 | className?: string
9 | }
10 |
11 | export function CodeEditor({
12 | language,
13 | filename,
14 | code,
15 | className,
16 | }: CodeEditorProps) {
17 | return (
18 |
19 | {filename && (
20 |
21 |
22 | {filename}
23 |
24 |
25 | )}
26 |
27 | {code}
28 |
29 |
30 | )
31 | }
--------------------------------------------------------------------------------
/frontend/src/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4 |
5 | function Collapsible({
6 | ...props
7 | }: React.ComponentProps) {
8 | return
9 | }
10 |
11 | function CollapsibleTrigger({
12 | ...props
13 | }: React.ComponentProps) {
14 | return (
15 |
19 | )
20 | }
21 |
22 | function CollapsibleContent({
23 | ...props
24 | }: React.ComponentProps) {
25 | return (
26 |
30 | )
31 | }
32 |
33 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
34 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Command as CommandPrimitive } from "cmdk"
3 | import { SearchIcon } from "lucide-react"
4 |
5 | import { cn } from "@/library/utils"
6 | import {
7 | Dialog,
8 | DialogContent,
9 | DialogDescription,
10 | DialogHeader,
11 | DialogTitle,
12 | } from "@/components/ui/dialog"
13 |
14 | function Command({
15 | className,
16 | ...props
17 | }: React.ComponentProps) {
18 | return (
19 |
27 | )
28 | }
29 |
30 | function CommandDialog({
31 | title = "Command Palette",
32 | description = "Search for a command to run...",
33 | children,
34 | ...props
35 | }: React.ComponentProps & {
36 | title?: string
37 | description?: string
38 | }) {
39 | return (
40 |
51 | )
52 | }
53 |
54 | function CommandInput({
55 | className,
56 | ...props
57 | }: React.ComponentProps) {
58 | return (
59 |
63 |
64 |
72 |
73 | )
74 | }
75 |
76 | function CommandList({
77 | className,
78 | ...props
79 | }: React.ComponentProps) {
80 | return (
81 |
89 | )
90 | }
91 |
92 | function CommandEmpty({
93 | ...props
94 | }: React.ComponentProps) {
95 | return (
96 |
101 | )
102 | }
103 |
104 | function CommandGroup({
105 | className,
106 | ...props
107 | }: React.ComponentProps) {
108 | return (
109 |
117 | )
118 | }
119 |
120 | function CommandSeparator({
121 | className,
122 | ...props
123 | }: React.ComponentProps) {
124 | return (
125 |
130 | )
131 | }
132 |
133 | function CommandItem({
134 | className,
135 | ...props
136 | }: React.ComponentProps) {
137 | return (
138 |
146 | )
147 | }
148 |
149 | function CommandShortcut({
150 | className,
151 | ...props
152 | }: React.ComponentProps<"span">) {
153 | return (
154 |
162 | )
163 | }
164 |
165 | export {
166 | Command,
167 | CommandDialog,
168 | CommandInput,
169 | CommandList,
170 | CommandEmpty,
171 | CommandGroup,
172 | CommandItem,
173 | CommandShortcut,
174 | CommandSeparator,
175 | }
176 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { XIcon } from "lucide-react"
6 |
7 | import { cn } from "@/library/utils"
8 |
9 | function Dialog({
10 | ...props
11 | }: React.ComponentProps) {
12 | return
13 | }
14 |
15 | function DialogTrigger({
16 | ...props
17 | }: React.ComponentProps) {
18 | return
19 | }
20 |
21 | function DialogPortal({
22 | ...props
23 | }: React.ComponentProps) {
24 | return
25 | }
26 |
27 | function DialogClose({
28 | ...props
29 | }: React.ComponentProps) {
30 | return
31 | }
32 |
33 | function DialogOverlay({
34 | className,
35 | ...props
36 | }: React.ComponentProps) {
37 | return (
38 |
46 | )
47 | }
48 |
49 | function DialogContent({
50 | className,
51 | children,
52 | ...props
53 | }: React.ComponentProps) {
54 | return (
55 |
56 |
57 |
65 | {children}
66 |
67 |
68 | Close
69 |
70 |
71 |
72 | )
73 | }
74 |
75 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
76 | return (
77 |
82 | )
83 | }
84 |
85 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
86 | return (
87 |
95 | )
96 | }
97 |
98 | function DialogTitle({
99 | className,
100 | ...props
101 | }: React.ComponentProps) {
102 | return (
103 |
108 | )
109 | }
110 |
111 | function DialogDescription({
112 | className,
113 | ...props
114 | }: React.ComponentProps) {
115 | return (
116 |
121 | )
122 | }
123 |
124 | export {
125 | Dialog,
126 | DialogClose,
127 | DialogContent,
128 | DialogDescription,
129 | DialogFooter,
130 | DialogHeader,
131 | DialogOverlay,
132 | DialogPortal,
133 | DialogTitle,
134 | DialogTrigger,
135 | }
136 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/library/utils"
4 |
5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6 | return (
7 |
18 | )
19 | }
20 |
21 | export { Input }
22 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 |
4 | import { cn } from "@/library/utils"
5 |
6 | function Label({
7 | className,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
19 | )
20 | }
21 |
22 | export { Label }
23 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { GripVerticalIcon } from "lucide-react"
3 | import * as ResizablePrimitive from "react-resizable-panels"
4 |
5 | import { cn } from "@/library/utils"
6 |
7 | function ResizablePanelGroup({
8 | className,
9 | ...props
10 | }: React.ComponentProps) {
11 | return (
12 |
20 | )
21 | }
22 |
23 | function ResizablePanel({
24 | ...props
25 | }: React.ComponentProps) {
26 | return
27 | }
28 |
29 | function ResizableHandle({
30 | withHandle,
31 | className,
32 | ...props
33 | }: React.ComponentProps & {
34 | withHandle?: boolean
35 | }) {
36 | return (
37 | div]:rotate-90",
41 | className
42 | )}
43 | {...props}
44 | >
45 | {withHandle && (
46 |
49 | )}
50 |
51 | )
52 | }
53 |
54 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
55 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SelectPrimitive from "@radix-ui/react-select"
3 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
4 |
5 | import { cn } from "@/library/utils"
6 |
7 | function Select({
8 | ...props
9 | }: React.ComponentProps) {
10 | return
11 | }
12 |
13 | function SelectGroup({
14 | ...props
15 | }: React.ComponentProps) {
16 | return
17 | }
18 |
19 | function SelectValue({
20 | ...props
21 | }: React.ComponentProps) {
22 | return
23 | }
24 |
25 | function SelectTrigger({
26 | className,
27 | children,
28 | ...props
29 | }: React.ComponentProps) {
30 | return (
31 |
39 | {children}
40 |
41 |
42 |
43 |
44 | )
45 | }
46 |
47 | function SelectContent({
48 | className,
49 | children,
50 | position = "popper",
51 | ...props
52 | }: React.ComponentProps) {
53 | return (
54 |
55 |
66 |
67 |
74 | {children}
75 |
76 |
77 |
78 |
79 | )
80 | }
81 |
82 | function SelectLabel({
83 | className,
84 | ...props
85 | }: React.ComponentProps) {
86 | return (
87 |
92 | )
93 | }
94 |
95 | function SelectItem({
96 | className,
97 | children,
98 | ...props
99 | }: React.ComponentProps) {
100 | return (
101 |
109 |
110 |
111 |
112 |
113 |
114 | {children}
115 |
116 | )
117 | }
118 |
119 | function SelectSeparator({
120 | className,
121 | ...props
122 | }: React.ComponentProps) {
123 | return (
124 |
129 | )
130 | }
131 |
132 | function SelectScrollUpButton({
133 | className,
134 | ...props
135 | }: React.ComponentProps) {
136 | return (
137 |
145 |
146 |
147 | )
148 | }
149 |
150 | function SelectScrollDownButton({
151 | className,
152 | ...props
153 | }: React.ComponentProps) {
154 | return (
155 |
163 |
164 |
165 | )
166 | }
167 |
168 | export {
169 | Select,
170 | SelectContent,
171 | SelectGroup,
172 | SelectItem,
173 | SelectLabel,
174 | SelectScrollDownButton,
175 | SelectScrollUpButton,
176 | SelectSeparator,
177 | SelectTrigger,
178 | SelectValue,
179 | }
180 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
3 |
4 | import { cn } from "@/library/utils"
5 |
6 | function Separator({
7 | className,
8 | orientation = "horizontal",
9 | decorative = true,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
23 | )
24 | }
25 |
26 | export { Separator }
27 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { XIcon } from "lucide-react"
6 |
7 | import { cn } from "@/library/utils"
8 |
9 | function Sheet({ ...props }: React.ComponentProps) {
10 | return
11 | }
12 |
13 | function SheetTrigger({
14 | ...props
15 | }: React.ComponentProps) {
16 | return
17 | }
18 |
19 | function SheetClose({
20 | ...props
21 | }: React.ComponentProps) {
22 | return
23 | }
24 |
25 | function SheetPortal({
26 | ...props
27 | }: React.ComponentProps) {
28 | return
29 | }
30 |
31 | function SheetOverlay({
32 | className,
33 | ...props
34 | }: React.ComponentProps) {
35 | return (
36 |
44 | )
45 | }
46 |
47 | function SheetContent({
48 | className,
49 | children,
50 | side = "right",
51 | ...props
52 | }: React.ComponentProps & {
53 | side?: "top" | "right" | "bottom" | "left"
54 | }) {
55 | return (
56 |
57 |
58 |
74 | {children}
75 |
76 |
77 | Close
78 |
79 |
80 |
81 | )
82 | }
83 |
84 | function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
85 | return (
86 |
91 | )
92 | }
93 |
94 | function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
95 | return (
96 |
101 | )
102 | }
103 |
104 | function SheetTitle({
105 | className,
106 | ...props
107 | }: React.ComponentProps) {
108 | return (
109 |
114 | )
115 | }
116 |
117 | function SheetDescription({
118 | className,
119 | ...props
120 | }: React.ComponentProps) {
121 | return (
122 |
127 | )
128 | }
129 |
130 | export {
131 | Sheet,
132 | SheetTrigger,
133 | SheetClose,
134 | SheetContent,
135 | SheetHeader,
136 | SheetFooter,
137 | SheetTitle,
138 | SheetDescription,
139 | }
140 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/library/utils"
2 |
3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4 | return (
5 |
10 | )
11 | }
12 |
13 | export { Skeleton }
14 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SliderPrimitive from "@radix-ui/react-slider"
3 |
4 | import { cn } from "@/library/utils"
5 |
6 | function Slider({
7 | className,
8 | defaultValue,
9 | value,
10 | min = 0,
11 | max = 100,
12 | ...props
13 | }: React.ComponentProps) {
14 | const _values = React.useMemo(
15 | () =>
16 | Array.isArray(value)
17 | ? value
18 | : Array.isArray(defaultValue)
19 | ? defaultValue
20 | : [min, max],
21 | [value, defaultValue, min, max]
22 | )
23 |
24 | return (
25 |
37 |
43 |
49 |
50 | {Array.from({ length: _values.length }, (_, index) => (
51 |
56 | ))}
57 |
58 | )
59 | }
60 |
61 | export { Slider }
62 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SwitchPrimitive from "@radix-ui/react-switch"
3 |
4 | import { cn } from "@/library/utils"
5 |
6 | function Switch({
7 | className,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
19 |
25 |
26 | )
27 | }
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TabsPrimitive from "@radix-ui/react-tabs"
3 |
4 | import { cn } from "@/library/utils"
5 |
6 | function Tabs({
7 | className,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
16 | )
17 | }
18 |
19 | function TabsList({
20 | className,
21 | ...props
22 | }: React.ComponentProps) {
23 | return (
24 |
32 | )
33 | }
34 |
35 | function TabsTrigger({
36 | className,
37 | ...props
38 | }: React.ComponentProps) {
39 | return (
40 |
48 | )
49 | }
50 |
51 | function TabsContent({
52 | className,
53 | ...props
54 | }: React.ComponentProps) {
55 | return (
56 |
61 | )
62 | }
63 |
64 | export { Tabs, TabsList, TabsTrigger, TabsContent }
65 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
3 |
4 | import { cn } from "@/library/utils"
5 |
6 | function TooltipProvider({
7 | delayDuration = 0,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
16 | )
17 | }
18 |
19 | function Tooltip({
20 | ...props
21 | }: React.ComponentProps) {
22 | return (
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | function TooltipTrigger({
30 | ...props
31 | }: React.ComponentProps) {
32 | return
33 | }
34 |
35 | function TooltipContent({
36 | className,
37 | sideOffset = 0,
38 | children,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
43 |
52 | {children}
53 |
54 |
55 |
56 | )
57 | }
58 |
59 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
60 |
--------------------------------------------------------------------------------
/frontend/src/hooks/use-mobile.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | /* @import "tw-animate-css"; */
3 |
4 | @custom-variant dark (&:is(.dark *));
5 |
6 | :root {
7 | --radius: 0.625rem;
8 | --background: oklch(1 0 0);
9 | --foreground: oklch(0.145 0 0);
10 | --card: oklch(1 0 0);
11 | --card-foreground: oklch(0.145 0 0);
12 | --popover: oklch(1 0 0);
13 | --popover-foreground: oklch(0.145 0 0);
14 | --primary: oklch(0.49 0.24 293);
15 | --primary-foreground: oklch(0.985 0 0);
16 | --secondary: oklch(0.89 0.05 293);
17 | --secondary-foreground: oklch(0.205 0 0);
18 | --muted: oklch(0.97 0 0);
19 | --muted-foreground: oklch(0.556 0 0);
20 | --accent: oklch(0.97 0 0);
21 | --accent-foreground: oklch(0.205 0 0);
22 | --destructive: oklch(0.577 0.245 27.325);
23 | --border: oklch(0.922 0 0);
24 | --input: oklch(0.38 0.18 294);
25 | --ring: oklch(0.708 0 0);
26 | --chart-1: oklch(0.646 0.222 41.116);
27 | --chart-2: oklch(0.6 0.118 184.704);
28 | --chart-3: oklch(0.398 0.07 227.392);
29 | --chart-4: oklch(0.828 0.189 84.429);
30 | --chart-5: oklch(0.769 0.188 70.08);
31 | --sidebar: oklch(0.985 0 0);
32 | --sidebar-foreground: oklch(0.145 0 0);
33 | --sidebar-primary: oklch(0.38 0.18 294);
34 | --sidebar-primary-foreground: oklch(0.985 0 0);
35 | --sidebar-accent: oklch(0.97 0 0);
36 | --sidebar-accent-foreground: oklch(0.205 0 0);
37 | --sidebar-border: oklch(0.922 0 0);
38 | --sidebar-ring: oklch(0.708 0 0);
39 | }
40 |
41 | .dark {
42 | --background: oklch(0.145 0 0);
43 | --foreground: oklch(0.985 0 0);
44 | --card: oklch(0.205 0 0);
45 | --card-foreground: oklch(0.985 0 0);
46 | --popover: oklch(0.205 0 0);
47 | --popover-foreground: oklch(0.985 0 0);
48 | --primary: oklch(0.922 0 0);
49 | --primary-foreground: oklch(0.205 0 0);
50 | --secondary: oklch(0.269 0 0);
51 | --secondary-foreground: oklch(0.985 0 0);
52 | --muted: oklch(0.269 0 0);
53 | --muted-foreground: oklch(0.708 0 0);
54 | --accent: oklch(0.269 0 0);
55 | --accent-foreground: oklch(0.985 0 0);
56 | --destructive: oklch(0.704 0.191 22.216);
57 | --border: oklch(1 0 0 / 10%);
58 | --input: oklch(1 0 0 / 15%);
59 | --ring: oklch(0.556 0 0);
60 | --chart-1: oklch(0.488 0.243 264.376);
61 | --chart-2: oklch(0.696 0.17 162.48);
62 | --chart-3: oklch(0.769 0.188 70.08);
63 | --chart-4: oklch(0.627 0.265 303.9);
64 | --chart-5: oklch(0.645 0.246 16.439);
65 | --sidebar: oklch(0.205 0 0);
66 | --sidebar-foreground: oklch(0.985 0 0);
67 | --sidebar-primary: oklch(0.488 0.243 264.376);
68 | --sidebar-primary-foreground: oklch(0.985 0 0);
69 | --sidebar-accent: oklch(0.269 0 0);
70 | --sidebar-accent-foreground: oklch(0.985 0 0);
71 | --sidebar-border: oklch(1 0 0 / 10%);
72 | --sidebar-ring: oklch(0.556 0 0);
73 | }
74 |
75 | @theme inline {
76 | --radius-sm: calc(var(--radius) - 4px);
77 | --radius-md: calc(var(--radius) - 2px);
78 | --radius-lg: var(--radius);
79 | --radius-xl: calc(var(--radius) + 4px);
80 | --color-background: var(--background);
81 | --color-foreground: var(--foreground);
82 | --color-card: var(--card);
83 | --color-card-foreground: var(--card-foreground);
84 | --color-popover: var(--popover);
85 | --color-popover-foreground: var(--popover-foreground);
86 | --color-primary: var(--primary);
87 | --color-primary-foreground: var(--primary-foreground);
88 | --color-secondary: var(--secondary);
89 | --color-secondary-foreground: var(--secondary-foreground);
90 | --color-muted: var(--muted);
91 | --color-muted-foreground: var(--muted-foreground);
92 | --color-accent: var(--accent);
93 | --color-accent-foreground: var(--accent-foreground);
94 | --color-destructive: var(--destructive);
95 | --color-border: var(--border);
96 | --color-input: var(--input);
97 | --color-ring: var(--ring);
98 | --color-chart-1: var(--chart-1);
99 | --color-chart-2: var(--chart-2);
100 | --color-chart-3: var(--chart-3);
101 | --color-chart-4: var(--chart-4);
102 | --color-chart-5: var(--chart-5);
103 | --color-sidebar: var(--sidebar);
104 | --color-sidebar-foreground: var(--sidebar-foreground);
105 | --color-sidebar-primary: var(--sidebar-primary);
106 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
107 | --color-sidebar-accent: var(--sidebar-accent);
108 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
109 | --color-sidebar-border: var(--sidebar-border);
110 | --color-sidebar-ring: var(--sidebar-ring);
111 | }
112 |
113 | @layer base {
114 | * {
115 | @apply border-border outline-ring/50;
116 | }
117 | body {
118 | @apply bg-background text-foreground;
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/frontend/src/library/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import './index.css'
4 | import App from './App.tsx'
5 |
6 | createRoot(document.getElementById('root')!).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/frontend/src/routeTree.gen.ts:
--------------------------------------------------------------------------------
1 | /* prettier-ignore-start */
2 |
3 | /* eslint-disable */
4 |
5 | // @ts-nocheck
6 |
7 | // noinspection JSUnusedGlobalSymbols
8 |
9 | // This file is auto-generated by TanStack Router
10 |
11 | // Import Routes
12 |
13 | import { Route as rootRoute } from './routes/__root'
14 | import { Route as SignupImport } from './routes/signup'
15 | import { Route as LoginImport } from './routes/login'
16 | import { Route as LayoutImport } from './routes/_layout'
17 | import { Route as LayoutIndexImport } from './routes/_layout/index'
18 | import { Route as LayoutTasksImport } from './routes/_layout/tasks'
19 | import { Route as LayoutSettingsImport } from './routes/_layout/settings'
20 | import { Route as LayoutAdminImport } from './routes/_layout/admin'
21 |
22 | // Create/Update Routes
23 |
24 | const SignupRoute = SignupImport.update({
25 | path: '/signup',
26 | getParentRoute: () => rootRoute,
27 | } as any)
28 |
29 | const LoginRoute = LoginImport.update({
30 | path: '/login',
31 | getParentRoute: () => rootRoute,
32 | } as any)
33 |
34 | const LayoutRoute = LayoutImport.update({
35 | id: '/_layout',
36 | getParentRoute: () => rootRoute,
37 | } as any)
38 |
39 | const LayoutIndexRoute = LayoutIndexImport.update({
40 | path: '/',
41 | getParentRoute: () => LayoutRoute,
42 | } as any)
43 |
44 | const LayoutTasksRoute = LayoutTasksImport.update({
45 | path: '/tasks',
46 | getParentRoute: () => LayoutRoute,
47 | } as any)
48 |
49 | const LayoutSettingsRoute = LayoutSettingsImport.update({
50 | path: '/settings',
51 | getParentRoute: () => LayoutRoute,
52 | } as any)
53 |
54 | const LayoutAdminRoute = LayoutAdminImport.update({
55 | path: '/admin',
56 | getParentRoute: () => LayoutRoute,
57 | } as any)
58 |
59 | // Populate the FileRoutesByPath interface
60 |
61 | declare module '@tanstack/react-router' {
62 | interface FileRoutesByPath {
63 | '/_layout': {
64 | preLoaderRoute: typeof LayoutImport
65 | parentRoute: typeof rootRoute
66 | }
67 | '/login': {
68 | preLoaderRoute: typeof LoginImport
69 | parentRoute: typeof rootRoute
70 | }
71 | '/signup': {
72 | preLoaderRoute: typeof SignupImport
73 | parentRoute: typeof rootRoute
74 | }
75 | '/_layout/admin': {
76 | preLoaderRoute: typeof LayoutAdminImport
77 | parentRoute: typeof LayoutImport
78 | }
79 | '/_layout/settings': {
80 | preLoaderRoute: typeof LayoutSettingsImport
81 | parentRoute: typeof LayoutImport
82 | }
83 | '/_layout/tasks': {
84 | preLoaderRoute: typeof LayoutTasksImport
85 | parentRoute: typeof LayoutImport
86 | }
87 | '/_layout/': {
88 | preLoaderRoute: typeof LayoutIndexImport
89 | parentRoute: typeof LayoutImport
90 | }
91 | }
92 | }
93 |
94 | // Create and export the route tree
95 |
96 | export const routeTree = rootRoute.addChildren([
97 | LayoutRoute.addChildren([
98 | LayoutAdminRoute,
99 | LayoutSettingsRoute,
100 | LayoutTasksRoute,
101 | LayoutIndexRoute,
102 | ]),
103 | LoginRoute,
104 | SignupRoute,
105 | ])
106 |
107 | /* prettier-ignore-end */
108 |
--------------------------------------------------------------------------------
/frontend/src/routes/__root.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet, createRootRoute } from "@tanstack/react-router"
2 | import React, { Suspense } from "react"
3 |
4 | import NotFound from "../components/Common/NotFound"
5 |
6 | const loadDevtools = () =>
7 | Promise.all([
8 | import("@tanstack/router-devtools"),
9 | import("@tanstack/react-query-devtools"),
10 | ]).then(([routerDevtools, reactQueryDevtools]) => {
11 | return {
12 | default: () => (
13 | <>
14 |
15 |
16 | >
17 | ),
18 | }
19 | })
20 |
21 | const TanStackDevtools =
22 | process.env.NODE_ENV === "production" ? () => null : React.lazy(loadDevtools)
23 |
24 | export const Route = createRootRoute({
25 | component: () => (
26 | <>
27 |
28 |
29 |
30 |
31 | >
32 | ),
33 | notFoundComponent: () => ,
34 | })
35 |
--------------------------------------------------------------------------------
/frontend/src/routes/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, Spinner } from "@chakra-ui/react"
2 | import { Outlet, createFileRoute, redirect } from "@tanstack/react-router"
3 |
4 | import Sidebar from "../components/Common/Sidebar"
5 | import UserMenu from "../components/Common/UserMenu"
6 | import useAuth, { isLoggedIn } from "../hooks/useAuth"
7 |
8 | export const Route = createFileRoute("/_layout")({
9 | component: Layout,
10 | beforeLoad: async () => {
11 | if (!isLoggedIn()) {
12 | throw redirect({
13 | to: "/login",
14 | })
15 | }
16 | },
17 | })
18 |
19 | function Layout() {
20 | const { isLoading } = useAuth()
21 |
22 | return (
23 |
24 |
25 | {isLoading ? (
26 |
27 |
28 |
29 | ) : (
30 |
31 | )}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/frontend/src/routes/_layout/admin.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Badge,
3 | Box,
4 | Button,
5 | Container,
6 | Flex,
7 | Heading,
8 | SkeletonText,
9 | Table,
10 | TableContainer,
11 | Tbody,
12 | Td,
13 | Th,
14 | Thead,
15 | Tr,
16 | } from "@chakra-ui/react"
17 | import { useQuery, useQueryClient } from "@tanstack/react-query"
18 | import { createFileRoute, useNavigate } from "@tanstack/react-router"
19 | import { useEffect } from "react"
20 | import { z } from "zod"
21 |
22 | import { type UserPublic, UsersService } from "../../client"
23 | import AddUser from "../../components/Admin/AddUser"
24 | import ActionsMenu from "../../components/Common/ActionsMenu"
25 | import Navbar from "../../components/Common/Navbar"
26 |
27 | const usersSearchSchema = z.object({
28 | page: z.number().catch(1),
29 | })
30 |
31 | export const Route = createFileRoute("/_layout/admin")({
32 | component: Admin,
33 | validateSearch: (search) => usersSearchSchema.parse(search),
34 | })
35 |
36 | const PER_PAGE = 5
37 |
38 | function getUsersQueryOptions({ page }: { page: number }) {
39 | return {
40 | queryFn: () =>
41 | UsersService.readUsers({ skip: (page - 1) * PER_PAGE, limit: PER_PAGE }),
42 | queryKey: ["users", { page }],
43 | }
44 | }
45 |
46 | function UsersTable() {
47 | const queryClient = useQueryClient()
48 | const currentUser = queryClient.getQueryData(["currentUser"])
49 | const { page } = Route.useSearch()
50 | const navigate = useNavigate({ from: Route.fullPath })
51 | const setPage = (page: number) =>
52 | navigate({
53 | search: (prev: z.infer) => ({
54 | ...prev,
55 | page,
56 | }),
57 | })
58 |
59 | const {
60 | data: users,
61 | isPending,
62 | isPlaceholderData,
63 | } = useQuery({
64 | ...getUsersQueryOptions({ page }),
65 | placeholderData: (prevData) => prevData,
66 | })
67 |
68 | const hasNextPage = !isPlaceholderData && users?.data.length === PER_PAGE
69 | const hasPreviousPage = page > 1
70 |
71 | useEffect(() => {
72 | if (hasNextPage) {
73 | queryClient.prefetchQuery(getUsersQueryOptions({ page: page + 1 }))
74 | }
75 | }, [page, queryClient, hasNextPage])
76 |
77 | return (
78 | <>
79 |
80 |
81 |
82 |
83 | Full name |
84 | Email |
85 | Role |
86 | Status |
87 | Actions |
88 |
89 |
90 | {isPending ? (
91 |
92 |
93 | {new Array(4).fill(null).map((_, index) => (
94 |
95 |
96 | |
97 | ))}
98 |
99 |
100 | ) : (
101 |
102 | {users?.data.map((user) => (
103 |
104 |
109 | {user.full_name || "N/A"}
110 | {currentUser?.id === user.id && (
111 |
112 | You
113 |
114 | )}
115 | |
116 |
117 | {user.email}
118 | |
119 | {user.is_superuser ? "Superuser" : "User"} |
120 |
121 |
122 |
129 | {user.is_active ? "Active" : "Inactive"}
130 |
131 | |
132 |
133 |
138 | |
139 |
140 | ))}
141 |
142 | )}
143 |
144 |
145 |
152 |
155 | Page {page}
156 |
159 |
160 | >
161 | )
162 | }
163 |
164 | function Admin() {
165 | return (
166 |
167 |
168 | Users Management
169 |
170 |
171 |
172 |
173 |
174 | )
175 | }
176 |
--------------------------------------------------------------------------------
/frontend/src/routes/_layout/index.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Container, Text } from "@chakra-ui/react"
2 | import { createFileRoute } from "@tanstack/react-router"
3 |
4 | import useAuth from "../../hooks/useAuth"
5 |
6 | export const Route = createFileRoute("/_layout/")({
7 | component: Dashboard,
8 | })
9 |
10 | function Dashboard() {
11 | const { user: currentUser } = useAuth()
12 |
13 | return (
14 | <>
15 |
16 |
17 |
18 | Hi, {currentUser?.full_name || currentUser?.email} 👋🏼
19 |
20 | Welcome back, nice to see you again!
21 |
22 |
23 | >
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/routes/_layout/settings.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Container,
3 | Heading,
4 | Tab,
5 | TabList,
6 | TabPanel,
7 | TabPanels,
8 | Tabs,
9 | } from "@chakra-ui/react"
10 | import { useQueryClient } from "@tanstack/react-query"
11 | import { createFileRoute } from "@tanstack/react-router"
12 |
13 | import type { UserPublic } from "../../client"
14 | import Appearance from "../../components/UserSettings/Appearance"
15 | import ChangePassword from "../../components/UserSettings/ChangePassword"
16 | import DeleteAccount from "../../components/UserSettings/DeleteAccount"
17 | import UserInformation from "../../components/UserSettings/UserInformation"
18 |
19 | const tabsConfig = [
20 | { title: "My profile", component: UserInformation },
21 | { title: "Password", component: ChangePassword },
22 | { title: "Appearance", component: Appearance },
23 | { title: "Danger zone", component: DeleteAccount },
24 | ]
25 |
26 | export const Route = createFileRoute("/_layout/settings")({
27 | component: UserSettings,
28 | })
29 |
30 | function UserSettings() {
31 | const queryClient = useQueryClient()
32 | const currentUser = queryClient.getQueryData(["currentUser"])
33 | const finalTabs = currentUser?.is_superuser
34 | ? tabsConfig.slice(0, 3)
35 | : tabsConfig
36 |
37 | return (
38 |
39 |
40 | User Settings
41 |
42 |
43 |
44 | {finalTabs.map((tab, index) => (
45 | {tab.title}
46 | ))}
47 |
48 |
49 | {finalTabs.map((tab, index) => (
50 |
51 |
52 |
53 | ))}
54 |
55 |
56 |
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/frontend/src/routes/_layout/tasks.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | Container,
5 | Flex,
6 | Heading,
7 | SkeletonText,
8 | Table,
9 | TableContainer,
10 | Tbody,
11 | Td,
12 | Th,
13 | Thead,
14 | Tr,
15 | Progress,
16 | } from "@chakra-ui/react"
17 | import { useQuery, useQueryClient } from "@tanstack/react-query"
18 | import { createFileRoute, useNavigate } from "@tanstack/react-router"
19 | import { useEffect } from "react"
20 | import { z } from "zod"
21 |
22 | import {Task, TasksService} from "../../client"
23 | import ActionsMenu from "../../components/Common/ActionsMenu"
24 | import Navbar from "../../components/Common/Navbar"
25 | import AddTask from "../../components/Tasks/AddTask"
26 |
27 | const tasksSearchSchema = z.object({
28 | page: z.number().catch(1),
29 | })
30 |
31 | export const Route = createFileRoute("/_layout/tasks")({
32 | component: Tasks,
33 | validateSearch: (search) => tasksSearchSchema.parse(search),
34 | })
35 |
36 | interface AutoRefetchProps {
37 | refetch: () => void
38 | intervalMs: number
39 | shouldRefetch: boolean
40 | }
41 |
42 | export function useAutoRefetch({ refetch, intervalMs, shouldRefetch }: AutoRefetchProps) {
43 | useEffect(() => {
44 | if (!shouldRefetch) return
45 |
46 | const interval = setInterval(() => {
47 | refetch()
48 | }, intervalMs)
49 |
50 | return () => {
51 | clearInterval(interval) // 确保定时器被清理
52 | }
53 | }, [refetch, intervalMs, shouldRefetch])
54 | }
55 |
56 | const PER_PAGE = 5
57 |
58 | function getTasksQueryOptions({ page }: { page: number }) {
59 | return {
60 | queryFn: () =>
61 | TasksService.getMyTasks({ skip: (page - 1) * PER_PAGE, limit: PER_PAGE }),
62 | queryKey: ["tasks", { page }],
63 | }
64 | }
65 |
66 | function TasksTable() {
67 | const queryClient = useQueryClient()
68 | const { page } = Route.useSearch()
69 | const navigate = useNavigate({ from: Route.fullPath })
70 | const setPage = (page: number) =>
71 | navigate({ search: (prev) => ({ ...prev, page }) })
72 |
73 | const {
74 | data: tasks,
75 | isPending,
76 | isPlaceholderData,
77 | refetch,
78 | } = useQuery({
79 | ...getTasksQueryOptions({ page }),
80 | placeholderData: (prevData) => prevData,
81 | })
82 |
83 | const hasNextPage = false
84 | const hasPreviousPage = page > 1
85 |
86 | const shouldRefetch = !!(tasks?.data && tasks.data.length > 0)
87 |
88 | useAutoRefetch({ refetch, intervalMs: 3000, shouldRefetch })
89 |
90 | useEffect(() => {
91 | if (hasNextPage) {
92 | queryClient.prefetchQuery(getTasksQueryOptions({ page: page + 1 }))
93 | }
94 | }, [page, queryClient, hasNextPage])
95 |
96 | return (
97 | <>
98 |
99 |
100 |
101 |
102 | Task ID |
103 | Status |
104 | SubTasks |
105 | Progress |
106 | Error |
107 | Action |
108 |
109 |
110 | {isPending ? (
111 |
112 | {new Array(5).fill(null).map((_, rowIndex) => (
113 |
114 | {new Array(5).fill(null).map((_, colIndex) => (
115 |
116 |
117 | |
118 | ))}
119 |
120 | ))}
121 |
122 | ) : (
123 |
124 | {tasks?.data && tasks?.data.map((task: Task) => (
125 |
126 | {task.task_id} |
127 |
128 |
129 |
136 | {task.status}
137 |
138 | |
139 |
140 | {task.progress?.total}
141 | |
142 |
143 | {task.progress?.percentage || 0}%
149 | |
150 |
151 | {task.error || "N/A"}
152 | |
153 |
154 |
159 | |
160 |
161 | ))}
162 |
163 | )}
164 |
165 |
166 |
173 |
176 | Page {page}
177 |
180 |
181 | >
182 | )
183 | }
184 |
185 | function Tasks() {
186 | return (
187 |
188 |
189 | Tasks Management
190 |
191 |
192 |
193 |
194 |
195 | )
196 | }
197 |
--------------------------------------------------------------------------------
/frontend/src/routes/login.tsx:
--------------------------------------------------------------------------------
1 | import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons"
2 | import {
3 | Button,
4 | Container,
5 | FormControl,
6 | FormErrorMessage,
7 | Icon,
8 | Image,
9 | Input,
10 | InputGroup,
11 | InputRightElement,
12 | Link,
13 | Text,
14 | useBoolean,
15 | } from "@chakra-ui/react"
16 | import {
17 | Link as RouterLink,
18 | createFileRoute,
19 | redirect,
20 | } from "@tanstack/react-router"
21 | import { type SubmitHandler, useForm } from "react-hook-form"
22 |
23 | import Logo from "/assets/images/camel-logo.svg"
24 | import type { Body_login_login_access_token as AccessToken } from "../client"
25 | import useAuth, { isLoggedIn } from "../hooks/useAuth"
26 | import { emailPattern } from "../utils"
27 |
28 | export const Route = createFileRoute("/login")({
29 | component: Login,
30 | beforeLoad: async () => {
31 | if (isLoggedIn()) {
32 | throw redirect({
33 | to: "/",
34 | })
35 | }
36 | },
37 | })
38 |
39 | function Login() {
40 | const [show, setShow] = useBoolean()
41 | const { loginMutation, error, resetError } = useAuth()
42 | const {
43 | register,
44 | handleSubmit,
45 | formState: { errors, isSubmitting },
46 | } = useForm({
47 | mode: "onBlur",
48 | criteriaMode: "all",
49 | defaultValues: {
50 | username: "",
51 | password: "",
52 | },
53 | })
54 |
55 | const onSubmit: SubmitHandler = async (data) => {
56 | if (isSubmitting) return
57 |
58 | resetError()
59 |
60 | try {
61 | await loginMutation.mutateAsync(data)
62 | } catch {
63 | // error is handled by useAuth hook
64 | }
65 | }
66 |
67 | return (
68 | <>
69 |
79 |
87 |
88 |
98 | {errors.username && (
99 | {errors.username.message}
100 | )}
101 |
102 |
103 |
104 |
112 |
118 |
123 | {show ? : }
124 |
125 |
126 |
127 | {error && {error}}
128 |
129 |
132 |
133 | Don't have an account?{" "}
134 |
135 | Sign up
136 |
137 |
138 |
139 | >
140 | )
141 | }
142 |
--------------------------------------------------------------------------------
/frontend/src/routes/signup.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Container,
4 | Flex,
5 | FormControl,
6 | FormErrorMessage,
7 | FormLabel,
8 | Image,
9 | Input,
10 | Link,
11 | Text,
12 | } from "@chakra-ui/react"
13 | import {
14 | Link as RouterLink,
15 | createFileRoute,
16 | redirect,
17 | } from "@tanstack/react-router"
18 | import { type SubmitHandler, useForm } from "react-hook-form"
19 |
20 | import Logo from "/assets/images/camel-logo.svg"
21 | import type { UserRegister } from "../client"
22 | import useAuth, { isLoggedIn } from "../hooks/useAuth"
23 | import { confirmPasswordRules, emailPattern, passwordRules } from "../utils"
24 |
25 | export const Route = createFileRoute("/signup")({
26 | component: SignUp,
27 | beforeLoad: async () => {
28 | if (isLoggedIn()) {
29 | throw redirect({
30 | to: "/",
31 | })
32 | }
33 | },
34 | })
35 |
36 | interface UserRegisterForm extends UserRegister {
37 | confirm_password: string
38 | }
39 |
40 | function SignUp() {
41 | const { signUpMutation } = useAuth()
42 | const {
43 | register,
44 | handleSubmit,
45 | getValues,
46 | formState: { errors, isSubmitting },
47 | } = useForm({
48 | mode: "onBlur",
49 | criteriaMode: "all",
50 | defaultValues: {
51 | email: "",
52 | full_name: "",
53 | password: "",
54 | confirm_password: "",
55 | },
56 | })
57 |
58 | const onSubmit: SubmitHandler = (data) => {
59 | signUpMutation.mutate(data)
60 | }
61 |
62 | return (
63 | <>
64 |
65 |
75 |
83 |
84 |
85 | Full Name
86 |
87 |
94 | {errors.full_name && (
95 | {errors.full_name.message}
96 | )}
97 |
98 |
99 |
100 | Email
101 |
102 |
111 | {errors.email && (
112 | {errors.email.message}
113 | )}
114 |
115 |
116 |
117 | Password
118 |
119 |
125 | {errors.password && (
126 | {errors.password.message}
127 | )}
128 |
129 |
133 |
134 | Confirm Password
135 |
136 |
137 |
143 | {errors.confirm_password && (
144 |
145 | {errors.confirm_password.message}
146 |
147 | )}
148 |
149 |
152 |
153 | Already have an account?{" "}
154 |
155 | Log In
156 |
157 |
158 |
159 |
160 | >
161 | )
162 | }
163 |
164 | export default SignUp
165 |
--------------------------------------------------------------------------------
/frontend/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/frontend/tests/auth.setup.ts:
--------------------------------------------------------------------------------
1 | import { test as setup } from "@playwright/test"
2 |
3 | const authFile = "playwright/.auth/user.json"
4 |
5 | setup("basic page test", async ({ page }) => {
6 | // 访问主页
7 | await page.goto("/")
8 |
9 | // 等待页面加载完成
10 | await page.waitForSelector('.chat-interface', { state: 'visible', timeout: 10000 })
11 |
12 | // 保存状态
13 | await page.context().storageState({ path: authFile })
14 | })
15 |
--------------------------------------------------------------------------------
/frontend/tests/camel.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from "@playwright/test"
2 |
3 | // 使用 auth.setup.ts 中设置的状态
4 | test.use({ storageState: 'playwright/.auth/user.json' })
5 |
6 | test.describe("Camel Agent Chat", () => {
7 | test.beforeEach(async ({ page }) => {
8 | await page.goto("/")
9 | // 等待聊天界面加载
10 | await page.waitForSelector('.chat-interface', { state: 'visible' })
11 | })
12 |
13 | test("should be able to chat with the agent", async ({ page }) => {
14 | // 找到并填写系统消息输入框
15 | const systemMessageInput = await page.locator('textarea').first()
16 | await systemMessageInput.fill("You are a helpful assistant")
17 |
18 | // 找到并填写用户消息输入框
19 | const userMessageInput = await page.locator('.chat-input input')
20 | await userMessageInput.fill("Hello, who are you?")
21 |
22 | // 提交消息
23 | await page.keyboard.press('Enter')
24 |
25 | // 等待响应出现在聊天历史中
26 | await expect(
27 | page.locator('.chat-message.assistant').first()
28 | ).toBeVisible({ timeout: 30000 })
29 | })
30 |
31 | test("should display chat history correctly", async ({ page }) => {
32 | // 发送消息
33 | const userMessageInput = await page.locator('.chat-input input')
34 | await userMessageInput.fill("Test message")
35 | await page.keyboard.press('Enter')
36 |
37 | // 验证用户消息和AI响应都显示在历史记录中
38 | await expect(page.locator('.chat-message.user')).toBeVisible()
39 | await expect(page.locator('.chat-message.assistant')).toBeVisible()
40 | })
41 |
42 | test("should handle model selection", async ({ page }) => {
43 | // 选择不同的模型
44 | const modelSelect = await page.locator('select').first()
45 | await modelSelect.selectOption('GPT_3_5_TURBO')
46 |
47 | // 发送消息并验证响应
48 | const userMessageInput = await page.locator('.chat-input input')
49 | await userMessageInput.fill("Hello with GPT-3.5")
50 | await page.keyboard.press('Enter')
51 |
52 | // 验证响应
53 | await expect(
54 | page.locator('.chat-message.assistant').first()
55 | ).toBeVisible({ timeout: 30000 })
56 | })
57 |
58 | test("should handle API errors gracefully", async ({ page }) => {
59 | await page.goto("/")
60 |
61 | // 模拟无效的系统消息
62 | await page.getByLabel("System Message").fill("")
63 | await page.getByPlaceholder("输入消息...").fill("This should fail")
64 | await page.getByRole("button", { name: "发送" }).click()
65 |
66 | // 验证错误消息显示
67 | await expect(page.getByText("抱歉,发生了错误")).toBeVisible()
68 | })
69 | })
--------------------------------------------------------------------------------
/frontend/tests/config.ts:
--------------------------------------------------------------------------------
1 | import path from "node:path"
2 | import { fileURLToPath } from "node:url"
3 | import dotenv from "dotenv"
4 |
5 | const __filename = fileURLToPath(import.meta.url)
6 | const __dirname = path.dirname(__filename)
7 |
8 | dotenv.config({ path: path.join(__dirname, "../../.env") })
9 |
10 | const { FIRST_SUPERUSER, FIRST_SUPERUSER_PASSWORD } = process.env
11 |
12 | if (typeof FIRST_SUPERUSER !== "string") {
13 | throw new Error("Environment variable FIRST_SUPERUSER is undefined")
14 | }
15 |
16 | if (typeof FIRST_SUPERUSER_PASSWORD !== "string") {
17 | throw new Error("Environment variable FIRST_SUPERUSER_PASSWORD is undefined")
18 | }
19 |
20 | export const firstSuperuser = FIRST_SUPERUSER as string
21 | export const firstSuperuserPassword = FIRST_SUPERUSER_PASSWORD as string
22 |
--------------------------------------------------------------------------------
/frontend/tests/home.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from "@playwright/test"
2 |
3 | test.describe("Home Page", () => {
4 | test("should load the main page", async ({ page }) => {
5 | await page.goto("/")
6 |
7 | // 验证页面标题存在
8 | await expect(page.getByRole("heading", { name: "Welcome to My App" })).toBeVisible()
9 |
10 | // 在这里添加更多的测试用例
11 | // 例如:验证特定组件是否存在、交互是否正常等
12 | })
13 | })
--------------------------------------------------------------------------------
/frontend/tests/utils/random.ts:
--------------------------------------------------------------------------------
1 | export const randomEmail = () =>
2 | `test_${Math.random().toString(36).substring(7)}@example.com`
3 |
4 | export const randomTeamName = () =>
5 | `Team ${Math.random().toString(36).substring(7)}`
6 |
7 | export const randomPassword = () => `${Math.random().toString(36).substring(2)}`
8 |
9 | export const slugify = (text: string) =>
10 | text
11 | .toLowerCase()
12 | .replace(/\s+/g, "-")
13 | .replace(/[^\w-]+/g, "")
14 |
--------------------------------------------------------------------------------
/frontend/tests/utils/user.ts:
--------------------------------------------------------------------------------
1 | import { type Page, expect } from "@playwright/test"
2 |
3 | export async function signUpNewUser(
4 | page: Page,
5 | name: string,
6 | email: string,
7 | password: string,
8 | ) {
9 | await page.goto("/signup")
10 |
11 | await page.getByPlaceholder("Full Name").fill(name)
12 | await page.getByPlaceholder("Email").fill(email)
13 | await page.getByPlaceholder("Password", { exact: true }).fill(password)
14 | await page.getByPlaceholder("Repeat Password").fill(password)
15 | await page.getByRole("button", { name: "Sign Up" }).click()
16 | await expect(
17 | page.getByText("Your account has been created successfully"),
18 | ).toBeVisible()
19 | await page.goto("/login")
20 | }
21 |
22 | export async function logInUser(page: Page, email: string, password: string) {
23 | await page.goto("/login")
24 |
25 | await page.getByPlaceholder("Email").fill(email)
26 | await page.getByPlaceholder("Password", { exact: true }).fill(password)
27 | await page.getByRole("button", { name: "Log In" }).click()
28 | await page.waitForURL("/")
29 | await expect(
30 | page.getByText("Welcome back, nice to see you again!"),
31 | ).toBeVisible()
32 | }
33 |
34 | export async function logOutUser(page: Page) {
35 | await page.getByTestId("user-menu").click()
36 | await page.getByRole("menuitem", { name: "Log out" }).click()
37 | await page.goto("/login")
38 | }
39 |
--------------------------------------------------------------------------------
/frontend/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": [
7 | "./src/*"
8 | ]
9 | },
10 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
11 | "target": "ES2020",
12 | "useDefineForClassFields": true,
13 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
14 | "module": "ESNext",
15 | "skipLibCheck": true,
16 |
17 | /* Bundler mode */
18 | "moduleResolution": "bundler",
19 | "allowImportingTsExtensions": true,
20 | "isolatedModules": true,
21 | "moduleDetection": "force",
22 | "noEmit": true,
23 | "jsx": "react-jsx",
24 |
25 | /* Linting */
26 | "strict": true,
27 | "noUnusedLocals": true,
28 | "noUnusedParameters": true,
29 | "noFallthroughCasesInSwitch": true,
30 | "noUncheckedSideEffectImports": true
31 | },
32 | "include": ["src"]
33 | }
34 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ],
7 | "compilerOptions": {
8 |
9 |
10 | "baseUrl": ".",
11 | "paths": {
12 | "@/*": ["./src/*"]
13 | }
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/frontend/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "skipLibCheck": true,
4 | "module": "ESNext",
5 | "moduleResolution": "bundler",
6 | "allowSyntheticDefaultImports": true,
7 |
8 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
9 | "target": "ES2022",
10 | "lib": ["ES2023"],
11 |
12 | "allowImportingTsExtensions": true,
13 | "isolatedModules": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 |
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedSideEffectImports": true
22 | },
23 | "include": ["vite.config.ts"]
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/vite.config.ts:
--------------------------------------------------------------------------------
1 | import react from "@vitejs/plugin-react-swc"
2 | import tailwindcss from "@tailwindcss/vite"
3 | import path from "path"
4 | import { defineConfig } from "vite"
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig({
8 | plugins: [react(),tailwindcss()],
9 | resolve: {
10 | alias: {
11 | "@": path.resolve(__dirname, "./src"),
12 | },
13 | },
14 | })
15 |
--------------------------------------------------------------------------------
/hooks/post_gen_project.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 |
4 | path: Path
5 | for path in Path(".").glob("**/*.sh"):
6 | data = path.read_bytes()
7 | lf_data = data.replace(b"\r\n", b"\n")
8 | path.write_bytes(lf_data)
9 |
--------------------------------------------------------------------------------
/scripts/build-push.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env sh
2 |
3 | # Exit in case of error
4 | set -e
5 |
6 | TAG=${TAG?Variable not set} \
7 | FRONTEND_ENV=${FRONTEND_ENV-production} \
8 | sh ./scripts/build.sh
9 |
10 | docker-compose -f docker-compose.yml push
11 |
--------------------------------------------------------------------------------
/scripts/build.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env sh
2 |
3 | # Exit in case of error
4 | set -e
5 |
6 | TAG=${TAG?Variable not set} \
7 | FRONTEND_ENV=${FRONTEND_ENV-production} \
8 | docker-compose \
9 | -f docker-compose.yml \
10 | build
11 |
--------------------------------------------------------------------------------
/scripts/deploy.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env sh
2 |
3 | # Exit in case of error
4 | set -e
5 |
6 | DOMAIN=${DOMAIN?Variable not set} \
7 | STACK_NAME=${STACK_NAME?Variable not set} \
8 | TAG=${TAG?Variable not set} \
9 | docker-compose \
10 | -f docker-compose.yml \
11 | config > docker-stack.yml
12 |
13 | docker-auto-labels docker-stack.yml
14 |
15 | docker stack deploy -c docker-stack.yml --with-registry-auth "${STACK_NAME?Variable not set}"
16 |
--------------------------------------------------------------------------------
/scripts/test-local.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 |
3 | # Exit in case of error
4 | set -e
5 |
6 | docker-compose down -v --remove-orphans # Remove possibly previous broken stacks left hanging after an error
7 |
8 | if [ $(uname -s) = "Linux" ]; then
9 | echo "Remove __pycache__ files"
10 | sudo find . -type d -name __pycache__ -exec rm -r {} \+
11 | fi
12 |
13 | docker-compose build
14 | docker-compose up -d
15 | docker-compose exec -T backend bash /app/tests-start.sh "$@"
16 |
--------------------------------------------------------------------------------
/scripts/test.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env sh
2 |
3 | # Exit in case of error
4 | set -e
5 | set -x
6 |
7 | docker compose build
8 | docker compose down -v --remove-orphans # Remove possibly previous broken stacks left hanging after an error
9 | docker compose up -d
10 | docker compose exec -T backend bash /app/tests-start.sh "$@"
11 | docker compose down -v --remove-orphans
12 |
--------------------------------------------------------------------------------