├── .env.template ├── .github └── workflows │ ├── backend.yml │ ├── deploy.yml │ └── template.yml ├── .gitignore ├── .terraform.lock.hcl ├── Makefile ├── README.md ├── backend ├── .env.template ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── poetry.lock ├── pyproject.toml ├── server.py └── src │ ├── auth.py │ ├── e2b_tool.py │ ├── open_interpreter.py │ └── settings.py ├── docker-compose.yml ├── frontend ├── .env.template ├── .eslintrc.json ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── app │ ├── api │ │ ├── create-sandbox │ │ │ └── route.ts │ │ └── upload-file │ │ │ └── route.ts │ ├── auth │ │ └── callback │ │ │ └── route.ts │ ├── error │ │ └── page.tsx │ ├── globals.css │ ├── layout.tsx │ ├── middleware.ts │ ├── opengraph-image.png │ ├── page.tsx │ ├── privacypolicy │ │ └── page.js │ ├── termsofservice │ │ └── page.js │ └── twitter-image.png ├── assets │ └── fonts │ │ ├── Inter-Bold.woff │ │ └── Inter-Regular.woff ├── components │ ├── agent-selector.tsx │ ├── anon-shield.tsx │ ├── button-scroll-to-bottom.tsx │ ├── chat-list.tsx │ ├── chat-message-actions.tsx │ ├── chat-message.tsx │ ├── chat-panel.tsx │ ├── chat-scroll-anchor.tsx │ ├── chat.tsx │ ├── empty-screen.tsx │ ├── external-link.tsx │ ├── feedback.tsx │ ├── header.tsx │ ├── login-button.tsx │ ├── markdown.tsx │ ├── prompt-form.tsx │ ├── providers.tsx │ ├── tailwind-indicator.tsx │ ├── ui │ │ ├── button.tsx │ │ ├── codeblock.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── icons.tsx │ │ ├── separator.tsx │ │ └── tooltip.tsx │ └── user-menu.tsx ├── lib │ ├── agents.ts │ ├── constants.ts │ ├── fonts.ts │ ├── hooks │ │ ├── use-agent.tsx │ │ ├── use-at-bottom.tsx │ │ ├── use-copy-to-clipboard.tsx │ │ └── use-enter-submit.tsx │ └── utils.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prettier.config.cjs ├── public │ ├── agentboard_logo.png │ ├── android-chrome-192x192.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-48x48.png │ ├── favicon-old.ico │ ├── favicon.ico │ └── thirteen.svg ├── tailwind.config.js ├── tsconfig.json └── utils │ ├── posthog-server.ts │ └── supabase │ ├── client.ts │ ├── middleware.ts │ └── server.ts ├── main.tf ├── sandbox-template ├── README.md ├── e2b.Dockerfile ├── e2b.toml └── requirements.txt ├── supabase-auth.png ├── terraform ├── github-action │ ├── main.tf │ └── variables.tf └── init │ ├── main.tf │ ├── outputs.tf │ └── variables.tf └── variables.tf /.env.template: -------------------------------------------------------------------------------- 1 | # Required for Terraform 2 | GCP_PROJECT_ID= 3 | GCP_REGION= 4 | GCP_ZONE= 5 | TERRAFORM_STATE_BUCKET= 6 | -------------------------------------------------------------------------------- /.github/workflows/backend.yml: -------------------------------------------------------------------------------- 1 | name: Backend server 2 | 3 | env: 4 | IMAGE: e2b-agentboard/backend 5 | 6 | on: 7 | workflow_call: 8 | secrets: 9 | service_account_email: 10 | required: true 11 | workload_identity_provider: 12 | required: true 13 | gcp_project_id: 14 | required: true 15 | 16 | jobs: 17 | publish: 18 | name: Build & push 19 | defaults: 20 | run: 21 | working-directory: ./backend 22 | runs-on: ubuntu-22.04 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Setup Service Account 28 | uses: google-github-actions/auth@v2 29 | with: 30 | workload_identity_provider: ${{ secrets.workload_identity_provider }} 31 | service_account: ${{ secrets.service_account_email }} 32 | 33 | - name: Configure Docker 34 | run: gcloud --quiet auth configure-docker us-central1-docker.pkg.dev 35 | 36 | - name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@v2 38 | 39 | - name: Build and Push docker images 40 | run: docker build --platform linux/amd64 --tag "${{ vars.GCP_REGION }}-docker.pkg.dev/${{ vars.E2B_GCP_PROJECT_ID }}/$(IMAGE)" --push . 41 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | id-token: write 10 | contents: write 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | 15 | env: 16 | TF_PLUGIN_CACHE_DIR: ${{ github.workspace }}/.terraform.d/plugin-cache 17 | 18 | jobs: 19 | changes: 20 | name: Repository changes 21 | runs-on: ubuntu-22.04 22 | outputs: 23 | get-version: ${{ steps.get-version.outputs.version }} 24 | backend: ${{ steps.filter.outputs.backend }} 25 | terraform: ${{ steps.filter.outputs.terraform }} 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 0 31 | 32 | - name: Get the last release 33 | id: last_release 34 | uses: cardinalby/git-get-release-action@v1 35 | env: 36 | GITHUB_TOKEN: ${{ github.token }} 37 | with: 38 | latest: true 39 | prerelease: false 40 | draft: false 41 | 42 | - name: Find changes since the last release 43 | uses: dorny/paths-filter@v2 44 | id: filter 45 | with: 46 | base: ${{ steps.last_release.outputs.tag_name }} 47 | filters: | 48 | backend: 49 | - 'backend/**' 50 | - '.github/workflows/backend.yml' 51 | terraform: 52 | - '**/*.tf' 53 | 54 | - name: Get next version 55 | id: get-version 56 | run: | 57 | version=${{ steps.last_release.outputs.tag_name }} 58 | result=$(echo ${version} | awk -F. -v OFS=. '{$NF += 1 ; print}') 59 | echo "::set-output name=version::$result" 60 | 61 | backend: 62 | name: Backend server 63 | needs: changes 64 | if: | 65 | always() && 66 | needs.changes.outputs.backend == 'true' 67 | uses: ./.github/workflows/backend.yml 68 | secrets: 69 | workload_identity_provider: ${{ secrets.E2B_WORKLOAD_IDENTITY_PROVIDER }} 70 | service_account_email: ${{ vars.E2B_SERVICE_ACCOUNT_EMAIL }} 71 | gcp_project_id: ${{ secrets.E2B_GCP_PROJECT_ID }} 72 | 73 | deploy: 74 | name: Deploy 75 | needs: [backend] 76 | if: | 77 | always() && ( 78 | needs.changes.outputs.terraform == 'true' || 79 | needs.backend.result == 'success' 80 | ) 81 | runs-on: ubuntu-22.04 82 | steps: 83 | - name: Checkout repository 84 | uses: actions/checkout@v4 85 | 86 | - name: Setup Service Account 87 | uses: google-github-actions/auth@v2 88 | with: 89 | create_credentials_file: true 90 | service_account: ${{ vars.E2B_SERVICE_ACCOUNT_EMAIL }} 91 | workload_identity_provider: ${{ secrets.E2B_WORKLOAD_IDENTITY_PROVIDER }} 92 | 93 | - name: Setup Terraform 94 | uses: hashicorp/setup-terraform@v2 95 | with: 96 | terraform_version: 1.5.7 97 | 98 | - name: Create Terraform Plugin Cache Dir 99 | run: mkdir --parents $TF_PLUGIN_CACHE_DIR 100 | 101 | - name: Init Terraform 102 | run: make init 103 | env: 104 | GCP_PROJECT_ID: ${{ vars.E2B_GCP_PROJECT_ID }} 105 | GCP_REGION: ${{ vars.E2B_GCP_REGION }} 106 | GCP_ZONE: ${{ vars.E2B_GCP_ZONE }} 107 | PREFIX: ${{ vars.E2B_PREFIX }} 108 | TERRAFORM_STATE_BUCKET: ${{ vars.E2B_TERRAFORM_STATE_BUCKET }} 109 | EXCLUDE_GITHUB: 0 110 | 111 | - name: Deploy Terraform 112 | run: make apply 113 | env: 114 | GCP_PROJECT_ID: ${{ vars.E2B_GCP_PROJECT_ID }} 115 | GCP_REGION: ${{ vars.E2B_GCP_REGION }} 116 | GCP_ZONE: ${{ vars.E2B_GCP_ZONE }} 117 | PREFIX: ${{ vars.E2B_PREFIX }} 118 | TERRAFORM_STATE_BUCKET: ${{ vars.E2B_TERRAFORM_STATE_BUCKET }} 119 | EXCLUDE_GITHUB: 0 120 | 121 | # The last successful release is used for determining which changed and what should be deployed in this release. 122 | release: 123 | name: Release 124 | needs: [changes, deploy] 125 | if: | 126 | always() && 127 | needs.deploy.result == 'success' 128 | runs-on: ubuntu-22.04 129 | steps: 130 | - name: Create release 131 | uses: ncipollo/release-action@v1 132 | with: 133 | name: API ${{ needs.changes.outputs.get-version }} 134 | tag: ${{ needs.changes.outputs.get-version }} 135 | commit: main 136 | generateReleaseNotes: true 137 | -------------------------------------------------------------------------------- /.github/workflows/template.yml: -------------------------------------------------------------------------------- 1 | name: Build and push Code Interpreter templates 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'sandbox-template/**' 7 | - '.github/workflows/template.yml' 8 | branches: 9 | - main 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | buildAndPublish: 16 | defaults: 17 | run: 18 | working-directory: ./sandbox-template 19 | 20 | name: Build and Push Images 21 | runs-on: ubuntu-20.04 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v3 25 | 26 | - name: Set up Docker Buildx 27 | uses: docker/setup-buildx-action@v3 28 | 29 | - name: Install E2B CLI 30 | run: npm install -g @e2b/cli 31 | 32 | - name: Build e2b 33 | run: e2b template build 34 | env: 35 | E2B_ACCESS_TOKEN: ${{ secrets.E2B_ACCESS_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | agent_frameworks 3 | .idea 4 | .vscode 5 | .env 6 | .next 7 | -------------------------------------------------------------------------------- /.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/google" { 5 | version = "5.6.0" 6 | constraints = "5.6.0" 7 | hashes = [ 8 | "h1:qhj3soOCQLWWtz4PRZp68q0sJ1F7+JSCBhGualLOEjk=", 9 | "zh:102b6a2672fade82114eb14ed46923fb1b74be2aaca3a50b4f35f7057a9a94b9", 10 | "zh:1a56b63175068c67efbe7d130986ba2839a938f5ffc96a14fd450153174dbfa3", 11 | "zh:1ba1c5e0c86e8aaa8037406390846e78c89b63faf9e527c7874641f35d436e1b", 12 | "zh:3f7161b9288b47cbe89d2f9675f78d83b58ad5880c793b01f50a71ee2583844b", 13 | "zh:66912d6e4180dac37185d17424b345a9d4e3c3c791d45e0737b35e32c9536b35", 14 | "zh:6f06f56e9fac2e55b50e74ffac42d9522bb379394e51dca1eddd4c3b7a68545c", 15 | "zh:8741861ebfa13bb1ed74ea7f4865388a0725ca3a781b6d873ce45e6a4630fe41", 16 | "zh:ae89a9c538665fbc30bb83aa3b13acb18d8380e551ccf242e1c0ab4d626089ab", 17 | "zh:c510f8321c7599aa601b1870fdc0c76cbad3054ed5cc70fe8e37a13a8046a71f", 18 | "zh:cf143a53d5a25c6216d09a9c0b115bb473ffcebd5c4c62b2b2594b1ebc13e662", 19 | "zh:de05b957e5dfdbaf92db47cd9b3ef46a0f8d94599eea6d472928f33058856add", 20 | "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", 21 | ] 22 | } 23 | 24 | provider "registry.terraform.io/hashicorp/random" { 25 | version = "3.5.1" 26 | constraints = "3.5.1" 27 | hashes = [ 28 | "h1:IL9mSatmwov+e0+++YX2V6uel+dV6bn+fC/cnGDK3Ck=", 29 | "zh:04e3fbd610cb52c1017d282531364b9c53ef72b6bc533acb2a90671957324a64", 30 | "zh:119197103301ebaf7efb91df8f0b6e0dd31e6ff943d231af35ee1831c599188d", 31 | "zh:4d2b219d09abf3b1bb4df93d399ed156cadd61f44ad3baf5cf2954df2fba0831", 32 | "zh:6130bdde527587bbe2dcaa7150363e96dbc5250ea20154176d82bc69df5d4ce3", 33 | "zh:6cc326cd4000f724d3086ee05587e7710f032f94fc9af35e96a386a1c6f2214f", 34 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 35 | "zh:b6d88e1d28cf2dfa24e9fdcc3efc77adcdc1c3c3b5c7ce503a423efbdd6de57b", 36 | "zh:ba74c592622ecbcef9dc2a4d81ed321c4e44cddf7da799faa324da9bf52a22b2", 37 | "zh:c7c5cde98fe4ef1143bd1b3ec5dc04baf0d4cc3ca2c5c7d40d17c0e9b2076865", 38 | "zh:dac4bad52c940cd0dfc27893507c1e92393846b024c5a9db159a93c534a3da03", 39 | "zh:de8febe2a2acd9ac454b844a4106ed295ae9520ef54dc8ed2faf29f12716b602", 40 | "zh:eab0d0495e7e711cca367f7d4df6e322e6c562fc52151ec931176115b83ed014", 41 | ] 42 | } 43 | 44 | provider "registry.terraform.io/integrations/github" { 45 | version = "5.42.0" 46 | constraints = "5.42.0" 47 | hashes = [ 48 | "h1:rfyLEgbZCk3MMCBuGd4PNFM914vtLqGIYcsmVKr6tdg=", 49 | "zh:0f97039c6b70295c4a82347bc8a0bcea700b3fb3df0e0be53585da025584bb7c", 50 | "zh:12e78898580cc2a72b5f2a77e191b158f88e974b0500489b691f34842288745c", 51 | "zh:23660933e4f00293c0d4d6cd6b4d72e382c0df46b70cecf22b5c4c090d3b61e3", 52 | "zh:74119174b46d8d197dd209a246bf8b5db113c66467e02c831e68a8ceea312d3e", 53 | "zh:829c4c0c202fc646eb0e1759eb9c8f0757df5295be2d3344b8fd6ca8ce9ef33b", 54 | "zh:92043e667f520aee4e08a10a183ad5abe5487f3e9c8ad5a55ea1358b14b17b1a", 55 | "zh:998909806b4ff42cf480fcd359ec1f12b868846f89284b991987f55de24876b7", 56 | "zh:9f758447db3bf386516562abd6da1e54d22ddc207bda25961d2b5b049f32da0f", 57 | "zh:a6259215612d4d6a281c671b2d5aa3a0a0b0a3ae92ed60b633998bb692e922d3", 58 | "zh:ad7d78056beb44191911db9443bf5eec41a3d60e7b01def2a9e608d1c4288d27", 59 | "zh:b697e7b0abef3000e1db482c897b82cd455621b488bb6c4cd3d270763d7b08ac", 60 | "zh:db8e849eded8aebff780f89ab7e1339053d2f15c1c8f94103d70266a090527ad", 61 | "zh:e5bdbb85fb148dd75877a7b94b595d4e8680e495c241db02c4b12b91e9d08953", 62 | "zh:ee812c5fd77d3817fb688f720e5eb42d7ff04db67a125de48b05458c9f657483", 63 | ] 64 | } 65 | 66 | provider "registry.terraform.io/kreuzwerker/docker" { 67 | version = "3.0.2" 68 | constraints = "3.0.2" 69 | hashes = [ 70 | "h1:XjdpVL61KtTsuPE8swok3GY8A+Bu3TZs8T2DOEpyiXo=", 71 | "zh:15b0a2b2b563d8d40f62f83057d91acb02cd0096f207488d8b4298a59203d64f", 72 | "zh:23d919de139f7cd5ebfd2ff1b94e6d9913f0977fcfc2ca02e1573be53e269f95", 73 | "zh:38081b3fe317c7e9555b2aaad325ad3fa516a886d2dfa8605ae6a809c1072138", 74 | "zh:4a9c5065b178082f79ad8160243369c185214d874ff5048556d48d3edd03c4da", 75 | "zh:5438ef6afe057945f28bce43d76c4401254073de01a774760169ac1058830ac2", 76 | "zh:60b7fadc287166e5c9873dfe53a7976d98244979e0ab66428ea0dea1ebf33e06", 77 | "zh:61c5ec1cb94e4c4a4fb1e4a24576d5f39a955f09afb17dab982de62b70a9bdd1", 78 | "zh:a38fe9016ace5f911ab00c88e64b156ebbbbfb72a51a44da3c13d442cd214710", 79 | "zh:c2c4d2b1fd9ebb291c57f524b3bf9d0994ff3e815c0cd9c9bcb87166dc687005", 80 | "zh:d567bb8ce483ab2cf0602e07eae57027a1a53994aba470fa76095912a505533d", 81 | "zh:e83bf05ab6a19dd8c43547ce9a8a511f8c331a124d11ac64687c764ab9d5a792", 82 | "zh:e90c934b5cd65516fbcc454c89a150bfa726e7cf1fe749790c7480bbeb19d387", 83 | "zh:f05f167d2eaf913045d8e7b88c13757e3cf595dd5cd333057fdafc7c4b7fed62", 84 | "zh:fcc9c1cea5ce85e8bcb593862e699a881bd36dffd29e2e367f82d15368659c3d", 85 | ] 86 | } 87 | 88 | provider "registry.terraform.io/vercel/vercel" { 89 | version = "1.4.0" 90 | constraints = "1.4.0" 91 | hashes = [ 92 | "h1:0VTUl782BBrd2J1NXkC0BRE2aJra8FkgNkZPJ99Jwjo=", 93 | "zh:0c37920908176f53927d36261118c98de83517d75395def499e1e3b21936dcc6", 94 | "zh:161be30b1aac8c2163f7bbfd251f12315ffaff396e238f13e316dec3f28a7a64", 95 | "zh:2ff52b478cf1fe5c77aabfd9e9489b63347522e3ef1957468b7311355bc7fac9", 96 | "zh:3439b6ddd17d9f12c4c121b3d3e86fbd3cac1828402b912b97c726cf057d22af", 97 | "zh:54d9770153ac2d1f510df30728b184593ae008c83995346418d8437e9ab89c14", 98 | "zh:589c1947393ba48d31e48233cb9780a47df9ce0ac806ca08d31d20d61986c5a7", 99 | "zh:62c77b622090e84c994cd455cfac767432a4348ee00df4afa2e936d243c8b2f3", 100 | "zh:63ac45f56365cf51caf98f2ca84eb81fd22cb04b9d85ca745605fd0b0172b879", 101 | "zh:95cc5dcfbee7f100c855f930296a556d547da8e853398e0464016dbec3ed4a79", 102 | "zh:a7de0dee68dccecee9a071f036c27b9cfc09e8e04af2ce84a8c438a6828f65d4", 103 | "zh:b572415a324f4d8612680d6fa1ca4f0dcf84834b75cbc0ac12f080a48fe086d2", 104 | "zh:b6e7a3fa2fa6cb39a1515be10131ac7d5ef21c2572ad01927e3b9fb80acffbe7", 105 | "zh:bf9ebcbefd488f76db5998dced0a4544057f53ffe8fb20ae65941242d5ce5ea1", 106 | "zh:f26e0763dbe6a6b2195c94b44696f2110f7f55433dc142839be16b9697fa5597", 107 | "zh:f60881e70ae2e0505bfd810a776625b4fdbef345593cfa103d5567939080e46b", 108 | ] 109 | } 110 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | -include .env 2 | 3 | tf_vars := TF_VAR_gcp_project_id=$(GCP_PROJECT_ID) \ 4 | TF_VAR_gcp_region=$(GCP_REGION) \ 5 | TF_VAR_gcp_zone=$(GCP_ZONE) \ 6 | TF_VAR_prefix=$(PREFIX) \ 7 | TF_VAR_terraform_state_bucket=$(TERRAFORM_STATE_BUCKET) 8 | 9 | .PHONY: init 10 | init: 11 | @ printf "Initializing Terraform\n\n" 12 | @ terraform init -reconfigure -input=false -backend-config="bucket=${TERRAFORM_STATE_BUCKET}" 13 | @ $(tf_vars) terraform apply -target=module.init -auto-approve -input=false -compact-warnings 14 | 15 | .PHONY: plan 16 | plan: 17 | @ printf "Planning Terraform\n\n" 18 | @ terraform fmt -recursive 19 | @ $(tf_vars) terraform plan -compact-warnings -detailed-exitcode 20 | 21 | .PHONY: apply 22 | apply: 23 | @ printf "Applying Terraform\n\n" 24 | @ $(tf_vars) \ 25 | terraform apply \ 26 | -auto-approve \ 27 | -input=false \ 28 | -compact-warnings 29 | 30 | .PHONY: run 31 | run: 32 | docker-compose up -d --build 33 | cd frontend && pnpm dev 34 | 35 | .PHONY: stop 36 | stop: 37 | docker-compose down 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Agentboard 2 | Agentboard is a project that allows you to use Open Interpreter in your browser. 3 | 4 | It's made with [E2B's sandbox](https://e2b.dev). Inside the sandbox, we use [Open Interpreter](https://openinterpreter.com/) to run code. 5 | 6 | ## Free hosted version 7 | [agentboard.dev](https://agentboard.dev) 8 | 9 | 10 | ## How to run app locally 11 | 12 | **Requirements** 13 | - Supabase project 14 | - Your Supabase project needs to have enabled [GitHub and Google auth providers](./supabase-auth.png) 15 | - [Poetry](https://python-poetry.org/) 16 | 17 | ### Steps 18 | **Frontend** 19 | 1. Copy `frontend/.env.example` to `frontend/.env` and set the correct env vars 20 | 1. Go to `frontend` 21 | 1. Install dependencies `pnpm i` 22 | 1. Start frontend `pnpm dev` 23 | 24 | **Backend** 25 | 1. Copy `backend/.env.example` to `backend/.env` and set the correct env vars 26 | 1. Install dependencies `poetry install` 27 | 1. Active poetry environment `poetry shell` 28 | 1. Start backend `uvicorn server:app --reload --port 8080` 29 | 30 | -------------------------------------------------------------------------------- /backend/.env.template: -------------------------------------------------------------------------------- 1 | # Required 2 | SUPABASE_URL= 3 | SUPABASE_KEY= 4 | OPENAI_API_KEY= 5 | E2B_API_KEY= 6 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | backend-env/ 2 | __pycache__/ -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-buster as builder 2 | 3 | 4 | ENV \ 5 | # Poetry 6 | POETRY_NO_INTERACTION=1 \ 7 | POETRY_VIRTUALENVS_CREATE=false \ 8 | POETRY_CACHE_DIR=/tmp/poetry_cache \ 9 | POETRY_VERSION=1.8.1 \ 10 | POETRY_HOME="/opt/poetry" \ 11 | # Python envs 12 | PYTHONUNBUFFERED=1 \ 13 | PYTHONDONTWRITEBYTECODE=1 \ 14 | # Pip 15 | PIP_NO_CACHE_DIR=off \ 16 | PIP_DISABLE_PIP_VERSION_CHECK=on \ 17 | PIP_DEFAULT_TIMEOUT=100 \ 18 | # Venv 19 | VIRTUAL_ENV="/venv" 20 | 21 | ENV PATH="$POETRY_HOME/bin:$VIRTUAL_ENV/bin:$PATH" 22 | ENV PYTHONPATH="/app:$PYTHONPATH" 23 | 24 | 25 | RUN curl -sSL https://install.python-poetry.org | python \ 26 | # configure poetry & make a virtualenv ahead of time since we only need one 27 | && python -m venv $VIRTUAL_ENV 28 | 29 | WORKDIR /code 30 | COPY poetry.lock pyproject.toml ./ 31 | 32 | RUN poetry install --no-root --only main && rm -rf $POETRY_CACHE_DIR 33 | 34 | COPY ./src src 35 | COPY ./server.py server.py 36 | 37 | ENTRYPOINT [ "uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8080"] 38 | -------------------------------------------------------------------------------- /backend/Makefile: -------------------------------------------------------------------------------- 1 | -include .env 2 | 3 | PHONY:build 4 | build: 5 | @docker build --platform linux/amd64 -t "${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}/${REPOSITORY_NAME}/backend:latest" . 6 | 7 | .PHONY:push 8 | push: 9 | @docker push "${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}/${REPOSITORY_NAME}/backend:latest" 10 | 11 | .PHONY:build-push 12 | build-push: build push 13 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Agentboard Backend 2 | 3 | Uses [Open Interpreter](https://github.com/OpenInterpreter/open-interpreter) running the code in [e2b](https://github.com/e2b-dev/E2B) sandboxes. 4 | 5 | ## Running backend locally 6 | 7 | 1. Make sure you have the correct `.env` file. 8 | 2. Install the dependencies with `poetry install` (if you don't have poetry, here's an [installation guide](https://python-poetry.org/docs/#installation). 9 | 3. Run the backend with `uvicorn server:app --reload --port 8080`. 10 | -------------------------------------------------------------------------------- /backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "agentboard-backend" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Jakub Novak "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.11" 10 | e2b = "^0.14.12" 11 | fastapi = "^0.110.0" 12 | pydantic = "^2.6.4" 13 | supabase = "^2.4.1" 14 | posthog = "^3.5.0" 15 | open-interpreter = "0.2.0" 16 | uvicorn = {extras = ["standard"], version = "^0.29.0"} 17 | e2b-code-interpreter = "^0.0.3" 18 | 19 | [tool.poetry.group.dev.dependencies] 20 | python-dotenv = "^1.0.1" 21 | black = "^24.3.0" 22 | 23 | [build-system] 24 | requires = ["poetry-core"] 25 | build-backend = "poetry.core.masonry.api" 26 | -------------------------------------------------------------------------------- /backend/server.py: -------------------------------------------------------------------------------- 1 | import urllib.parse 2 | import logging 3 | from typing import List, Annotated 4 | 5 | from fastapi import FastAPI, Depends, Security 6 | from fastapi.responses import StreamingResponse 7 | from fastapi.middleware.cors import CORSMiddleware 8 | from pydantic import BaseModel 9 | 10 | 11 | from interpreter.core.core import OpenInterpreter 12 | 13 | from src.auth import get_current_user 14 | from src.open_interpreter import get_interpreter 15 | from src.settings import posthog, models 16 | 17 | 18 | class EndpointFilter(logging.Filter): 19 | def filter(self, record: logging.LogRecord) -> bool: 20 | return record.args and len(record.args) >= 3 and record.args[2] != "/health" 21 | 22 | 23 | logging.basicConfig( 24 | level=logging.INFO, format="%(levelname)s - %(asctime)s - %(message)s" 25 | ) 26 | logger = logging.getLogger(__name__) 27 | # Add filter to the logger 28 | logging.getLogger("uvicorn.access").addFilter(EndpointFilter()) 29 | 30 | 31 | app = FastAPI() 32 | origins = [ 33 | "http://localhost:3000", 34 | "https://agentboard.dev", 35 | "https://www.agentboard.dev", 36 | "https://agentboard-git-staging-e2b.vercel.app", 37 | "https://agentboard-git-dev-e2b.vercel.app", 38 | "https://agentboard-git-gce-refactor-e2b.vercel.app", 39 | "https://agentboard-dev-git-refactor-e2b.vercel.app", 40 | ] 41 | app.add_middleware( 42 | CORSMiddleware, 43 | allow_origins=origins, 44 | allow_credentials=True, 45 | allow_methods=["*"], 46 | allow_headers=["*"], 47 | ) 48 | 49 | 50 | @app.get("/health") 51 | def health(): 52 | """ 53 | Health check endpoint. 54 | """ 55 | return {"status": "ok"} 56 | 57 | 58 | class UserMessage(BaseModel): 59 | role: str 60 | content: str 61 | 62 | 63 | class ChatRequest(BaseModel): 64 | messages: List[UserMessage] 65 | agent: str 66 | model: str 67 | 68 | 69 | @app.post("/chat") 70 | async def chat_endpoint( 71 | chat_request: ChatRequest, 72 | user_id: Annotated[str, Security(get_current_user)], 73 | interpreter: OpenInterpreter = Depends(get_interpreter()), 74 | ): 75 | model = chat_request.model 76 | if model not in models: 77 | raise Exception(f"Model {model} not found") 78 | interpreter.llm.model = models[model] 79 | 80 | agent = chat_request.agent 81 | if agent != "OPEN_INTERPRETER": 82 | raise Exception(f"Agent {agent} not found") 83 | 84 | # set messages to make this truly stateless 85 | logger.debug("chat_request.messages: " + str(chat_request.messages)) 86 | if len(chat_request.messages) > 1: 87 | interpreter.messages = [ 88 | {"role": x.role, "type": "message", "content": x.content} 89 | for x in chat_request.messages[:-1] 90 | ] 91 | else: 92 | interpreter.messages = [] 93 | 94 | logger.debug( 95 | "Interpreter messages before interpreter.chat():" + str(interpreter.messages) 96 | ) 97 | 98 | # record PostHog analytics 99 | posthog.capture( 100 | user_id, 101 | "chat_message_sent", 102 | {"messages": chat_request.messages[-1].content}, 103 | ) 104 | 105 | def event_stream(): 106 | other_content = [] 107 | for result in interpreter.chat( 108 | chat_request.messages[-1].content, stream=True, display=False 109 | ): 110 | if result: 111 | # get the first key and value in separate variables 112 | yieldval = "" 113 | 114 | if result["type"] == "code": 115 | if "start" in result and result["start"]: 116 | yieldval = f"\n```{result['format']}\n" 117 | elif "end" in result and result["end"]: 118 | yieldval = "\n```\n" 119 | else: 120 | yieldval = result["content"] 121 | elif result["type"] == "message": 122 | if "content" in result and result["content"]: 123 | yieldval = result["content"] 124 | elif result["type"] == "console": 125 | if "start" in result and result["start"]: 126 | yieldval = f"\n```shell output-bash\n" 127 | elif "end" in result and result["end"]: 128 | yieldval = "\n```\n" 129 | elif "format" in result: 130 | if result["format"] == "output": 131 | yieldval = result["content"] 132 | elif result["format"] == "image": 133 | other_content.append(f"\n```png\n{result['content']}\n```\n") 134 | elif result["type"] == "confirmation": 135 | pass 136 | else: 137 | raise Exception(f"Unknown result type: {result['type']}") 138 | yield f"{urllib.parse.quote(str(yieldval))}" 139 | for content in other_content: 140 | yield f"{urllib.parse.quote(str(content))}" 141 | return StreamingResponse(event_stream(), media_type="text/event-stream") 142 | -------------------------------------------------------------------------------- /backend/src/auth.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Annotated, Optional 3 | 4 | from fastapi import Header, HTTPException 5 | 6 | from src.settings import supabase 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | async def user_id_for_token(token: str) -> Optional[str]: 12 | """ 13 | Authenticate the token with Supabase and return the user ID if valid. 14 | """ 15 | # Remove 'Bearer ' prefix if present 16 | prefix = "Bearer " 17 | if token.startswith(prefix): 18 | token = token[len(prefix) :] 19 | 20 | # Verify the token with Supabase 21 | try: 22 | data = supabase.auth.get_user(token) 23 | if data: 24 | return data.user.id 25 | except Exception as e: 26 | logger.error(f"Supabase error when exchanging token for user id: {e}") 27 | return None 28 | 29 | 30 | async def get_current_user(authorization: str = Header(None)): 31 | if not authorization: 32 | logger.error("No authorization header") 33 | raise HTTPException(status_code=401, detail="No authorization header") 34 | 35 | # Exchange token for user ID 36 | user_id = await user_id_for_token(authorization) 37 | if not user_id: 38 | logger.error("Invalid token") 39 | raise HTTPException(status_code=401, detail="Invalid token") 40 | return user_id 41 | -------------------------------------------------------------------------------- /backend/src/e2b_tool.py: -------------------------------------------------------------------------------- 1 | import queue 2 | import threading 3 | import time 4 | from threading import Event 5 | from typing import Callable 6 | 7 | from e2b_code_interpreter import CodeInterpreter 8 | 9 | from src.settings import TIMEOUT 10 | 11 | 12 | def e2b_factory(sandbox_id): 13 | class PythonE2BSpecificSandbox: 14 | """ 15 | This class contains all requirements for being a custom language in Open Interpreter: 16 | 17 | - name (an attribute) 18 | - run (a method) 19 | - stop (a method) 20 | - terminate (a method) 21 | 22 | Here, we'll use E2B to power the `run` method. 23 | """ 24 | 25 | # This is the name that will appear to the LLM. 26 | name = "python" 27 | 28 | # Optionally, you can append some information about this language to the system message: 29 | system_message = """ 30 | You have access to python and internet, you can do whatever you want 31 | """ 32 | 33 | @staticmethod 34 | def run_in_background( 35 | code, on_message: Callable[[str], None], on_rich_data: Callable[[str], None], on_exit: Callable[[], None] 36 | ): 37 | with CodeInterpreter.reconnect(sandbox_id) as sandbox: 38 | execution = sandbox.notebook.exec_cell( 39 | code, 40 | on_stdout=lambda m: on_message(f"[stdout] {m.line}"), 41 | on_stderr=lambda m: on_message(f"[stderr] {m.line}"), 42 | ) 43 | 44 | if execution.error: 45 | message = f"There was an error during execution: {execution.error.name}: {execution.error.value}.\n{execution.error.traceback}" 46 | elif execution.results: 47 | message = "These are results of the execution:\n" 48 | for i, result in enumerate(execution.results): 49 | message += f"Result {i + 1}:\n" 50 | if result.is_main_result: 51 | message += f"[Main result]: {result.text}\n" 52 | else: 53 | message += f"[Display data]: {result.text}\n" 54 | if result.png: 55 | on_rich_data(result.png) 56 | 57 | message += f"It has also following formats: {result.formats()}\n" 58 | elif execution.logs.stdout or execution.logs.stderr: 59 | message = "Execution finished without any additional results" 60 | else: 61 | message = "There was no output of the execution." 62 | 63 | on_message(message) 64 | on_exit() 65 | 66 | def run(self, code): 67 | """Generator that yields a dictionary in LMC Format.""" 68 | yield { 69 | "type": "console", 70 | "format": "output", 71 | "content": "Running code in E2B...\n", 72 | } 73 | 74 | exit_event = Event() 75 | out_queue = queue.Queue[str]() 76 | images = queue.Queue[str]() 77 | 78 | threading.Thread( 79 | target=self.run_in_background, 80 | args=( 81 | code, 82 | lambda message: out_queue.put(message), 83 | lambda image: images.put(image), 84 | lambda: exit_event.set(), 85 | ), 86 | ).start() 87 | start_time = time.time() 88 | while not exit_event.is_set() or not out_queue.qsize() == 0: 89 | if time.time() - start_time > TIMEOUT: 90 | yield { 91 | "type": "console", 92 | "format": "output", 93 | "content": "Code execution timed out.\n", 94 | } 95 | break 96 | try: 97 | yield { 98 | "type": "console", 99 | "format": "output", 100 | "content": out_queue.get_nowait() + "\n", 101 | } 102 | out_queue.task_done() 103 | except queue.Empty: 104 | pass 105 | 106 | while not images.qsize() == 0: 107 | try: 108 | image_data = images.get_nowait() 109 | yield { 110 | "type": "console", 111 | "format": "image", 112 | "content": image_data, 113 | } 114 | images.task_done() 115 | except queue.Empty: 116 | pass 117 | 118 | def stop(self): 119 | """Stops the code.""" 120 | # Not needed here, because e2b.run_code isn't stateful. 121 | pass 122 | 123 | def terminate(self): 124 | """Terminates the entire process.""" 125 | # Not needed here, because e2b.run_code isn't stateful. 126 | pass 127 | 128 | return PythonE2BSpecificSandbox 129 | -------------------------------------------------------------------------------- /backend/src/open_interpreter.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | from e2b import Sandbox 4 | from fastapi import Security 5 | from interpreter import OpenInterpreter 6 | 7 | from src.auth import get_current_user 8 | from src.e2b_tool import e2b_factory 9 | 10 | 11 | def setup_interpreter(the_interpreter: OpenInterpreter, sandbox_id): 12 | the_interpreter.auto_run = True 13 | the_interpreter.llm.max_tokens = 4096 14 | the_interpreter.llm.context_window = 16385 15 | the_interpreter.computer.terminate() 16 | 17 | # Give Open Interpreter its languages. This will only let it run PythonE2B: 18 | the_interpreter.computer.languages = [e2b_factory(sandbox_id)] 19 | 20 | # Try it out! 21 | the_interpreter.system_message = """ 22 | 23 | # Who You Are 24 | You are a world-class programmer that can complete any goal or ask by executing code. 25 | 26 | # Rules 27 | First, write a plan. **Always recap the plan between each code block** (you have extreme short-term memory loss, so you need to recap the plan between each message block to retain it). 28 | The simpler the plan, the better, but it should include all of the steps necessary to accomplish the goal. 29 | 30 | You should try something, continue the plan in the steps you laid out earlier until the goal is accomplished. 31 | Never stop halfway through accomplishing your plan. 32 | 33 | If you generate image, it's automatically displayed to the user. Don't try to return base64 string to the user. 34 | 35 | When a task is complete and the final output file is written to disk, ALWAYS let the user know by 36 | using this EXACT syntax with no deviations: 37 | "`` is saved to disk. Download it here: [](/home/user/)." 38 | This will allow the user to download the file. 39 | 40 | Write messages to the user in Markdown. 41 | 42 | You can ONLY write Python code. All code you write will be executed in a Jupyter notebooks. 43 | 44 | # Your Capabilities 45 | You have **full and complete permission** to execute any code necessary to complete the task. 46 | 47 | Code blocks can access the internet. 48 | 49 | # Tips 50 | You have a few important Python packages already installed for you: 51 | * yt-dlp (Avoid using youtube-dl since its no longer maintained.) 52 | * pandas 53 | * beautifulsoup 54 | * numpy 55 | * moviepy (ffmpeg is installed at the system level) 56 | * PyPDF2 (to work with PDF files) 57 | * PIL (for image support) 58 | 59 | You can install new packages, you can access the internet, and you can write to disk. 60 | 61 | When the user refers to a filename, they're almost always referring to an existing file in the /home/user directory. 62 | Always prefer to use absolute paths when working with files. 63 | """ 64 | 65 | 66 | def sandbox_id_for_user_id(user_id: str): 67 | """ 68 | Search running sandboxes for the user's sandbox 69 | """ 70 | running_sandboxes = Sandbox.list() 71 | for running_sandbox in running_sandboxes: 72 | if ( 73 | running_sandbox.metadata 74 | and running_sandbox.metadata.get("userID", "") == user_id 75 | ): 76 | return running_sandbox.sandbox_id 77 | return None 78 | 79 | 80 | def get_interpreter(): 81 | """ 82 | This ensures that an interpreter instance is created for each request. 83 | """ 84 | 85 | def dependency(user_id: Annotated[str, Security(get_current_user)]): 86 | # Exchange user ID for sandbox ID 87 | sandbox_id = sandbox_id_for_user_id(user_id) 88 | if sandbox_id is None: 89 | return {"error": "No running sandbox found for user"} 90 | 91 | # Connect to the sandbox, keep it alive, and setup interpreter 92 | with Sandbox.reconnect(sandbox_id) as sandbox: 93 | sandbox.keep_alive(60 * 60) # max limit is 1 hour as of 2-13-24 94 | 95 | new_interpreter = OpenInterpreter() 96 | setup_interpreter(new_interpreter, sandbox_id) 97 | yield new_interpreter 98 | 99 | return dependency 100 | -------------------------------------------------------------------------------- /backend/src/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from posthog import Posthog 4 | from supabase import create_client 5 | 6 | env = os.getenv("ENV") 7 | is_prod = env == "prod" 8 | if not is_prod: 9 | from dotenv import load_dotenv 10 | 11 | load_dotenv() 12 | 13 | TIMEOUT = 30 14 | 15 | # Supabase client 16 | supabase = create_client(os.environ.get("SUPABASE_URL"), os.environ.get("SUPABASE_KEY")) 17 | 18 | # Posthog client 19 | if is_prod: 20 | posthog = Posthog(os.environ.get("POSTHOG_API_KEY"), os.environ.get("POSTHOG_HOST")) 21 | else: 22 | posthog = Posthog("", "", disabled=True) 23 | 24 | models = { 25 | "GPT-3.5": "gpt-3.5-turbo-0125", 26 | "GPT-4": "gpt-4-turbo-preview", 27 | } 28 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | backend: 3 | build: 4 | context: backend 5 | ports: 6 | - "8080:8080" 7 | env_file: backend/.env 8 | -------------------------------------------------------------------------------- /frontend/.env.template: -------------------------------------------------------------------------------- 1 | # Required 2 | NEXT_PUBLIC_SUPABASE_URL= 3 | NEXT_PUBLIC_SUPABASE_ANON_KEY= 4 | E2B_API_KEY= 5 | 6 | # Required for production 7 | NEXT_PUBLIC_AGENTBOARD_API_URL= 8 | 9 | # Optional 10 | NEXT_PUBLIC_POSTHOG_KEY= 11 | NEXT_PUBLIC_POSTHOG_HOST= 12 | -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/eslintrc", 3 | "root": true, 4 | "extends": [ 5 | "next/core-web-vitals", 6 | "prettier", 7 | "plugin:tailwindcss/recommended" 8 | ], 9 | "plugins": ["tailwindcss"], 10 | "rules": { 11 | "tailwindcss/no-custom-classname": "off" 12 | }, 13 | "settings": { 14 | "tailwindcss": { 15 | "callees": ["cn", "cva"], 16 | "config": "tailwind.config.js" 17 | } 18 | }, 19 | "overrides": [ 20 | { 21 | "files": ["*.ts", "*.tsx"], 22 | "parser": "@typescript-eslint/parser" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .pnpm-debug.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | 35 | .contentlayer 36 | .env 37 | .vercel 38 | .vscode -------------------------------------------------------------------------------- /frontend/.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true 2 | auto-install-peers=true 3 | exclude-links-from-lockfile=true 4 | prefer-workspace-packages=false 5 | link-workspace-packages=false -------------------------------------------------------------------------------- /frontend/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Vercel, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Agentboard Frontend 2 | 3 | Build on top of [Vercel AI Chat template](https://github.com/vercel/ai-chatbot) 4 | 5 | ## How to run the frontend locally 6 | 7 | 1. Make sure you have the correct `.env` file. 8 | 2. Run `pnpm dev`, it will start the frontend on `localhost:3000`. 9 | -------------------------------------------------------------------------------- /frontend/app/api/create-sandbox/route.ts: -------------------------------------------------------------------------------- 1 | import { Sandbox } from 'e2b' 2 | import { createClient } from '@/utils/supabase/server' 3 | 4 | export async function GET() { 5 | const supabase = createClient() 6 | const { 7 | data: { user } 8 | } = await supabase.auth.getUser() 9 | if (!user) { 10 | return new Response(JSON.stringify({ error: 'Unauthorized' }), { 11 | status: 401 12 | }) 13 | } 14 | 15 | let sandbox 16 | try { 17 | // connect to existing sandbox or create a new one 18 | const runningSandboxes = await Sandbox.list() 19 | const found = runningSandboxes.find(s => s.metadata?.userID === user.id) 20 | if (found) { 21 | // Sandbox found, we can reconnect to it 22 | sandbox = await Sandbox.reconnect(found.sandboxID) 23 | } else { 24 | // Sandbox not found, create a new one 25 | sandbox = await Sandbox.create({ 26 | template: 'ois-code-execution-sandbox', 27 | metadata: { userID: user.id } 28 | }) 29 | } 30 | 31 | // 5 minutes in development, 10 mins in production 32 | await sandbox.keepAlive( 33 | process.env.NODE_ENV === 'development' ? 2 * 60 * 1000 : 10 * 60 * 1000 34 | ) 35 | 36 | console.log('Sandbox created, id: ', sandbox.id) 37 | return new Response(JSON.stringify({ sandboxID: sandbox.id }), { 38 | headers: { 39 | 'Content-Type': 'application/json' 40 | }, 41 | status: 200 42 | }) 43 | } catch (e) { 44 | console.log('Error creating sandbox: ', e) 45 | return new Response(JSON.stringify(e), { 46 | status: 500 47 | }) 48 | } finally { 49 | if (sandbox) { 50 | await sandbox.close() 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /frontend/app/api/upload-file/route.ts: -------------------------------------------------------------------------------- 1 | import { Sandbox } from 'e2b' 2 | import { Writable, pipeline } from 'stream' 3 | import { promisify } from 'util' 4 | import { cookies } from 'next/headers' 5 | import { createClient } from '@/utils/supabase/server' 6 | 7 | export async function POST(req: Request) { 8 | const supabase = createClient() 9 | 10 | const { 11 | data: { user } 12 | } = await supabase.auth.getUser() 13 | if (!user) { 14 | return new Response('Unauthorized', { 15 | status: 401 16 | }) 17 | } 18 | 19 | let sandbox 20 | try { 21 | const formData = await req.formData() 22 | const file = formData.get('file') 23 | const fileName = formData.get('fileName') as string 24 | const sandboxID = formData.get('sandboxID') 25 | 26 | if (!file || typeof file === 'string') { 27 | return new Response(JSON.stringify({ error: 'No file uploaded' }), { 28 | status: 400 29 | }) 30 | } 31 | if (!sandboxID || typeof sandboxID !== 'string') { 32 | return new Response(JSON.stringify({ error: 'No sandbox ID provided' }), { 33 | status: 400 34 | }) 35 | } 36 | 37 | const fileBuffer = await file.arrayBuffer() 38 | const buffer = Buffer.from(fileBuffer) 39 | 40 | sandbox = await Sandbox.reconnect(sandboxID) 41 | await sandbox.keepAlive(3 * 60 * 1000) 42 | const remotePath = await sandbox.uploadFile(buffer, fileName) 43 | console.log( 44 | `The file was uploaded to '${remotePath}' path inside the sandbox ` 45 | ) 46 | console.log('/upload-file written to E2B filesystem') 47 | 48 | return new Response(JSON.stringify({ success: true }), { status: 200 }) 49 | } catch (e) { 50 | console.log(e) 51 | return new Response("Unexpected error, couldn't upload file", { 52 | status: 500 53 | }) 54 | } finally { 55 | if (sandbox) { 56 | await sandbox.close() 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /frontend/app/auth/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | import { type CookieOptions } from '@supabase/ssr' 3 | import { createClient } from '@/utils/supabase/server' 4 | 5 | export async function GET(request: Request) { 6 | const { searchParams, origin } = new URL(request.url) 7 | const code = searchParams.get('code') 8 | // if "next" is in param, use it as the redirect URL 9 | const next = searchParams.get('next') ?? '/' 10 | console.log('code:', code, 'next:', next, 'origin', origin) 11 | if (code) { 12 | const supabase = createClient() 13 | const { error } = await supabase.auth.exchangeCodeForSession(code) 14 | if (!error) { 15 | return NextResponse.redirect(`${origin}${next}`) 16 | } else { 17 | console.error('Error exchanging code for session:', error) 18 | } 19 | } 20 | 21 | // return the user to an error page with instructions 22 | return NextResponse.redirect(`${origin}/auth/auth-code-error`) 23 | } 24 | -------------------------------------------------------------------------------- /frontend/app/error/page.tsx: -------------------------------------------------------------------------------- 1 | export default function ErrorPage() { 2 | return

Sorry, something went wrong

3 | } 4 | -------------------------------------------------------------------------------- /frontend/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | 10 | --muted: 240 4.8% 95.9%; 11 | --muted-foreground: 240 3.8% 46.1%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 240 10% 3.9%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 240 10% 3.9%; 18 | 19 | --border: 240 5.9% 90%; 20 | --input: 240 5.9% 90%; 21 | 22 | --primary: 240 5.9% 10%; 23 | --primary-foreground: 0 0% 98%; 24 | 25 | --secondary: 240 4.8% 95.9%; 26 | --secondary-foreground: 240 5.9% 10%; 27 | 28 | --accent: 240 4.8% 95.9%; 29 | --accent-foreground: ; 30 | 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 0 0% 98%; 33 | 34 | --ring: 240 5% 64.9%; 35 | 36 | --radius: 0.5rem; 37 | } 38 | 39 | .dark { 40 | --background: 240 10% 3.9%; 41 | --foreground: 0 0% 98%; 42 | 43 | --muted: 240 3.7% 15.9%; 44 | --muted-foreground: 240 5% 64.9%; 45 | 46 | --popover: 240 10% 3.9%; 47 | --popover-foreground: 0 0% 98%; 48 | 49 | --card: 240 10% 3.9%; 50 | --card-foreground: 0 0% 98%; 51 | 52 | --border: 240 3.7% 15.9%; 53 | --input: 240 3.7% 15.9%; 54 | 55 | --primary: 0 0% 98%; 56 | --primary-foreground: 240 5.9% 10%; 57 | 58 | --secondary: 240 3.7% 15.9%; 59 | --secondary-foreground: 0 0% 98%; 60 | 61 | --accent: 240 3.7% 15.9%; 62 | --accent-foreground: ; 63 | 64 | --destructive: 0 62.8% 30.6%; 65 | --destructive-foreground: 0 85.7% 97.3%; 66 | 67 | --ring: 240 3.7% 15.9%; 68 | } 69 | } 70 | 71 | @layer base { 72 | * { 73 | @apply border-border; 74 | } 75 | body { 76 | @apply bg-background text-foreground; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /frontend/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata, Viewport } from 'next' 2 | 3 | import { Toaster } from 'react-hot-toast' 4 | import { Analytics } from '@vercel/analytics/react' 5 | 6 | import '@/app/globals.css' 7 | import { fontMono, fontSans } from '@/lib/fonts' 8 | import { cn } from '@/lib/utils' 9 | import { TailwindIndicator } from '@/components/tailwind-indicator' 10 | import { Providers } from '@/components/providers' 11 | import { Header } from '@/components/header' 12 | import { Feedback } from "@/components/feedback"; 13 | 14 | declare global { 15 | namespace JSX { 16 | interface IntrinsicElements { 17 | 'chatlio-widget': any 18 | } 19 | } 20 | } 21 | export const viewport: Viewport = { 22 | themeColor: [ 23 | { media: '(prefers-color-scheme: light)', color: 'white' }, 24 | { media: '(prefers-color-scheme: dark)', color: 'black' } 25 | ] 26 | } 27 | 28 | export const metadata: Metadata = { 29 | metadataBase: new URL(`https://www.agentboard.dev`), 30 | title: { 31 | default: 'Agentboard', 32 | template: `%s - Agentboard` 33 | }, 34 | description: 'Use AI agents in the browser', 35 | 36 | icons: { 37 | icon: '/favicon.ico', 38 | shortcut: '/favicon-16x16.png', 39 | apple: '/apple-touch-icon.png' 40 | }, 41 | openGraph: { 42 | images: '/opengraph-image.png' 43 | } 44 | } 45 | 46 | interface RootLayoutProps { 47 | children: React.ReactNode 48 | } 49 | 50 | export default function RootLayout({ children }: RootLayoutProps) { 51 | return ( 52 | 53 | 54 | 61 | 62 | 63 |
64 | {/* @ts-ignore */} 65 |
66 |
{children}
67 |
68 | 69 |
70 | 71 | 72 | 73 | 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /frontend/app/middleware.ts: -------------------------------------------------------------------------------- 1 | import { type NextRequest } from 'next/server' 2 | import { updateSession } from '@/utils/supabase/middleware' 3 | 4 | export async function middleware(request: NextRequest) { 5 | return await updateSession(request) 6 | } 7 | 8 | export const config = { 9 | matcher: [ 10 | /* 11 | * Match all request paths except for the ones starting with: 12 | * - _next/static (static files) 13 | * - _next/image (image optimization files) 14 | * - favicon.ico (favicon file) 15 | * Feel free to modify this pattern to include more paths. 16 | */ 17 | '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)' 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /frontend/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2b-dev/agentboard/d227c188a083b74d9699f4c9d5ddc53f72dedf95/frontend/app/opengraph-image.png -------------------------------------------------------------------------------- /frontend/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { createClient } from '@/utils/supabase/server' 2 | import { nanoid } from '@/lib/utils' 3 | import { Chat } from '@/components/chat' 4 | import { AnonShield } from '@/components/anon-shield' 5 | import PostHogClient from '@/utils/posthog-server' 6 | 7 | export default async function IndexPage() { 8 | // check if there is a session - if not, render the Anon Shield 9 | const supabase = createClient() 10 | 11 | const { 12 | data: { user } 13 | } = await supabase.auth.getUser() 14 | const posthog = PostHogClient() 15 | if (!user) { 16 | posthog.capture({ 17 | distinctId: 'anon', 18 | event: 'page_view' 19 | }) 20 | } else { 21 | posthog.capture({ 22 | distinctId: user.id, 23 | event: 'page_view' 24 | }) 25 | } 26 | 27 | const id = nanoid() 28 | 29 | return ( 30 | <> 31 | {!user && } 32 | 33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /frontend/app/privacypolicy/page.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Head from 'next/head' 3 | 4 | export default function PrivacyPolicyPage() { 5 | return ( 6 |
7 | 8 | 9 | 10 |

Privacy Policy

11 |

Last updated: December 18, 2023

12 |

13 | This Privacy Policy describes Our policies and procedures on the 14 | collection, use and disclosure of Your information when You use the 15 | Service and tells You about Your privacy rights and how the law protects 16 | You. 17 |

18 |

19 | We use Your Personal data to provide and improve the Service. By using 20 | the Service, You agree to the collection and use of information in 21 | accordance with this Privacy Policy. This Privacy Policy has been 22 | created with the help of the{' '} 23 | 27 | Free Privacy Policy Generator 28 | 29 | . 30 |

31 |

Interpretation and Definitions

32 |

Interpretation

33 |

34 | The words of which the initial letter is capitalized have meanings 35 | defined under the following conditions. The following definitions shall 36 | have the same meaning regardless of whether they appear in singular or 37 | in plural. 38 |

39 |

Definitions

40 |

For the purposes of this Privacy Policy:

41 |
    42 |
  • 43 |

    44 | Account means a unique account created for You to 45 | access our Service or parts of our Service. 46 |

    47 |
  • 48 |
  • 49 |

    50 | Affiliate means an entity that controls, is 51 | controlled by or is under common control with a party, where 52 | "control" means ownership of 50% or more of the shares, 53 | equity interest or other securities entitled to vote for election of 54 | directors or other managing authority. 55 |

    56 |
  • 57 |
  • 58 |

    59 | Company (referred to as either "the 60 | Company", "We", "Us" or "Our" in 61 | this Agreement) refers to Agentboard. 62 |

    63 |
  • 64 |
  • 65 |

    66 | Cookies are small files that are placed on Your 67 | computer, mobile device or any other device by a website, containing 68 | the details of Your browsing history on that website among its many 69 | uses. 70 |

    71 |
  • 72 |
  • 73 |

    74 | Country refers to: New York, United States 75 |

    76 |
  • 77 |
  • 78 |

    79 | Device means any device that can access the Service 80 | such as a computer, a cellphone or a digital tablet. 81 |

    82 |
  • 83 |
  • 84 |

    85 | Personal Data is any information that relates to an 86 | identified or identifiable individual. 87 |

    88 |
  • 89 |
  • 90 |

    91 | Service refers to the Website. 92 |

    93 |
  • 94 |
  • 95 |

    96 | Service Provider means any natural or legal person 97 | who processes the data on behalf of the Company. It refers to 98 | third-party companies or individuals employed by the Company to 99 | facilitate the Service, to provide the Service on behalf of the 100 | Company, to perform services related to the Service or to assist the 101 | Company in analyzing how the Service is used. 102 |

    103 |
  • 104 |
  • 105 |

    106 | Third-party Social Media Service refers to any 107 | website or any social network website through which a User can log 108 | in or create an account to use the Service. 109 |

    110 |
  • 111 |
  • 112 |

    113 | Usage Data refers to data collected automatically, 114 | either generated by the use of the Service or from the Service 115 | infrastructure itself (for example, the duration of a page visit). 116 |

    117 |
  • 118 |
  • 119 |

    120 | Website refers to Agentboard, accessible from{' '} 121 | 126 | https://agentboard.dev 127 | 128 |

    129 |
  • 130 |
  • 131 |

    132 | You means the individual accessing or using the 133 | Service, or the company, or other legal entity on behalf of which 134 | such individual is accessing or using the Service, as applicable. 135 |

    136 |
  • 137 |
138 |

Collecting and Using Your Personal Data

139 |

Types of Data Collected

140 |

Personal Data

141 |

142 | While using Our Service, We may ask You to provide Us with certain 143 | personally identifiable information that can be used to contact or 144 | identify You. Personally identifiable information may include, but is 145 | not limited to: 146 |

147 |
    148 |
  • 149 |

    Email address

    150 |
  • 151 |
  • 152 |

    First name and last name

    153 |
  • 154 |
  • 155 |

    Phone number

    156 |
  • 157 |
  • 158 |

    Usage Data

    159 |
  • 160 |
161 |

Usage Data

162 |

Usage Data is collected automatically when using the Service.

163 |

164 | Usage Data may include information such as Your Device's Internet 165 | Protocol address (e.g. IP address), browser type, browser version, the 166 | pages of our Service that You visit, the time and date of Your visit, 167 | the time spent on those pages, unique device identifiers and other 168 | diagnostic data. 169 |

170 |

171 | When You access the Service by or through a mobile device, We may 172 | collect certain information automatically, including, but not limited 173 | to, the type of mobile device You use, Your mobile device unique ID, the 174 | IP address of Your mobile device, Your mobile operating system, the type 175 | of mobile Internet browser You use, unique device identifiers and other 176 | diagnostic data. 177 |

178 |

179 | We may also collect information that Your browser sends whenever You 180 | visit our Service or when You access the Service by or through a mobile 181 | device. 182 |

183 |

Information from Third-Party Social Media Services

184 |

185 | The Company allows You to create an account and log in to use the 186 | Service through the following Third-party Social Media Services: 187 |

188 |
    189 |
  • Google
  • 190 |
  • Facebook
  • 191 |
  • Instagram
  • 192 |
  • Twitter
  • 193 |
  • LinkedIn
  • 194 |
195 |

196 | If You decide to register through or otherwise grant us access to a 197 | Third-Party Social Media Service, We may collect Personal data that is 198 | already associated with Your Third-Party Social Media Service's 199 | account, such as Your name, Your email address, Your activities or Your 200 | contact list associated with that account. 201 |

202 |

203 | You may also have the option of sharing additional information with the 204 | Company through Your Third-Party Social Media Service's account. If 205 | You choose to provide such information and Personal Data, during 206 | registration or otherwise, You are giving the Company permission to use, 207 | share, and store it in a manner consistent with this Privacy Policy. 208 |

209 |

Tracking Technologies and Cookies

210 |

211 | We use Cookies and similar tracking technologies to track the activity 212 | on Our Service and store certain information. Tracking technologies used 213 | are beacons, tags, and scripts to collect and track information and to 214 | improve and analyze Our Service. The technologies We use may include: 215 |

216 |
    217 |
  • 218 | Cookies or Browser Cookies. A cookie is a small file 219 | placed on Your Device. You can instruct Your browser to refuse all 220 | Cookies or to indicate when a Cookie is being sent. However, if You do 221 | not accept Cookies, You may not be able to use some parts of our 222 | Service. Unless you have adjusted Your browser setting so that it will 223 | refuse Cookies, our Service may use Cookies. 224 |
  • 225 |
  • 226 | Web Beacons. Certain sections of our Service and our 227 | emails may contain small electronic files known as web beacons (also 228 | referred to as clear gifs, pixel tags, and single-pixel gifs) that 229 | permit the Company, for example, to count users who have visited those 230 | pages or opened an email and for other related website statistics (for 231 | example, recording the popularity of a certain section and verifying 232 | system and server integrity). 233 |
  • 234 |
235 |

236 | Cookies can be "Persistent" or "Session" Cookies. 237 | Persistent Cookies remain on Your personal computer or mobile device 238 | when You go offline, while Session Cookies are deleted as soon as You 239 | close Your web browser. Learn more about cookies on the{' '} 240 | 244 | Free Privacy Policy website 245 | {' '} 246 | article. 247 |

248 |

249 | We use both Session and Persistent Cookies for the purposes set out 250 | below: 251 |

252 |
    253 |
  • 254 |

    255 | Necessary / Essential Cookies 256 |

    257 |

    Type: Session Cookies

    258 |

    Administered by: Us

    259 |

    260 | Purpose: These Cookies are essential to provide You with services 261 | available through the Website and to enable You to use some of its 262 | features. They help to authenticate users and prevent fraudulent use 263 | of user accounts. Without these Cookies, the services that You have 264 | asked for cannot be provided, and We only use these Cookies to 265 | provide You with those services. 266 |

    267 |
  • 268 |
  • 269 |

    270 | Cookies Policy / Notice Acceptance Cookies 271 |

    272 |

    Type: Persistent Cookies

    273 |

    Administered by: Us

    274 |

    275 | Purpose: These Cookies identify if users have accepted the use of 276 | cookies on the Website. 277 |

    278 |
  • 279 |
  • 280 |

    281 | Functionality Cookies 282 |

    283 |

    Type: Persistent Cookies

    284 |

    Administered by: Us

    285 |

    286 | Purpose: These Cookies allow us to remember choices You make when 287 | You use the Website, such as remembering your login details or 288 | language preference. The purpose of these Cookies is to provide You 289 | with a more personal experience and to avoid You having to re-enter 290 | your preferences every time You use the Website. 291 |

    292 |
  • 293 |
294 |

295 | For more information about the cookies we use and your choices regarding 296 | cookies, please visit our Cookies Policy or the Cookies section of our 297 | Privacy Policy. 298 |

299 |

Use of Your Personal Data

300 |

The Company may use Personal Data for the following purposes:

301 |
    302 |
  • 303 |

    304 | To provide and maintain our Service, including to 305 | monitor the usage of our Service. 306 |

    307 |
  • 308 |
  • 309 |

    310 | To manage Your Account: to manage Your registration 311 | as a user of the Service. The Personal Data You provide can give You 312 | access to different functionalities of the Service that are 313 | available to You as a registered user. 314 |

    315 |
  • 316 |
  • 317 |

    318 | For the performance of a contract: the development, 319 | compliance and undertaking of the purchase contract for the 320 | products, items or services You have purchased or of any other 321 | contract with Us through the Service. 322 |

    323 |
  • 324 |
  • 325 |

    326 | To contact You: To contact You by email, telephone 327 | calls, SMS, or other equivalent forms of electronic communication, 328 | such as a mobile application's push notifications regarding 329 | updates or informative communications related to the 330 | functionalities, products or contracted services, including the 331 | security updates, when necessary or reasonable for their 332 | implementation. 333 |

    334 |
  • 335 |
  • 336 |

    337 | To provide You with news, special offers and 338 | general information about other goods, services and events which we 339 | offer that are similar to those that you have already purchased or 340 | enquired about unless You have opted not to receive such 341 | information. 342 |

    343 |
  • 344 |
  • 345 |

    346 | To manage Your requests: To attend and manage Your 347 | requests to Us. 348 |

    349 |
  • 350 |
  • 351 |

    352 | For business transfers: We may use Your information 353 | to evaluate or conduct a merger, divestiture, restructuring, 354 | reorganization, dissolution, or other sale or transfer of some or 355 | all of Our assets, whether as a going concern or as part of 356 | bankruptcy, liquidation, or similar proceeding, in which Personal 357 | Data held by Us about our Service users is among the assets 358 | transferred. 359 |

    360 |
  • 361 |
  • 362 |

    363 | For other purposes: We may use Your information for 364 | other purposes, such as data analysis, identifying usage trends, 365 | determining the effectiveness of our promotional campaigns and to 366 | evaluate and improve our Service, products, services, marketing and 367 | your experience. 368 |

    369 |
  • 370 |
371 |

We may share Your personal information in the following situations:

372 |
    373 |
  • 374 | With Service Providers: We may share Your personal 375 | information with Service Providers to monitor and analyze the use of 376 | our Service, to contact You. 377 |
  • 378 |
  • 379 | For business transfers: We may share or transfer Your 380 | personal information in connection with, or during negotiations of, 381 | any merger, sale of Company assets, financing, or acquisition of all 382 | or a portion of Our business to another company. 383 |
  • 384 |
  • 385 | With Affiliates: We may share Your information with 386 | Our affiliates, in which case we will require those affiliates to 387 | honor this Privacy Policy. Affiliates include Our parent company and 388 | any other subsidiaries, joint venture partners or other companies that 389 | We control or that are under common control with Us. 390 |
  • 391 |
  • 392 | With business partners: We may share Your information 393 | with Our business partners to offer You certain products, services or 394 | promotions. 395 |
  • 396 |
  • 397 | With other users: when You share personal information 398 | or otherwise interact in the public areas with other users, such 399 | information may be viewed by all users and may be publicly distributed 400 | outside. If You interact with other users or register through a 401 | Third-Party Social Media Service, Your contacts on the Third-Party 402 | Social Media Service may see Your name, profile, pictures and 403 | description of Your activity. Similarly, other users will be able to 404 | view descriptions of Your activity, communicate with You and view Your 405 | profile. 406 |
  • 407 |
  • 408 | With Your consent: We may disclose Your personal 409 | information for any other purpose with Your consent. 410 |
  • 411 |
412 |

Retention of Your Personal Data

413 |

414 | The Company will retain Your Personal Data only for as long as is 415 | necessary for the purposes set out in this Privacy Policy. We will 416 | retain and use Your Personal Data to the extent necessary to comply with 417 | our legal obligations (for example, if we are required to retain your 418 | data to comply with applicable laws), resolve disputes, and enforce our 419 | legal agreements and policies. 420 |

421 |

422 | The Company will also retain Usage Data for internal analysis purposes. 423 | Usage Data is generally retained for a shorter period of time, except 424 | when this data is used to strengthen the security or to improve the 425 | functionality of Our Service, or We are legally obligated to retain this 426 | data for longer time periods. 427 |

428 |

Transfer of Your Personal Data

429 |

430 | Your information, including Personal Data, is processed at the 431 | Company's operating offices and in any other places where the 432 | parties involved in the processing are located. It means that this 433 | information may be transferred to — and maintained on — computers 434 | located outside of Your state, province, country or other governmental 435 | jurisdiction where the data protection laws may differ than those from 436 | Your jurisdiction. 437 |

438 |

439 | Your consent to this Privacy Policy followed by Your submission of such 440 | information represents Your agreement to that transfer. 441 |

442 |

443 | The Company will take all steps reasonably necessary to ensure that Your 444 | data is treated securely and in accordance with this Privacy Policy and 445 | no transfer of Your Personal Data will take place to an organization or 446 | a country unless there are adequate controls in place including the 447 | security of Your data and other personal information. 448 |

449 |

Delete Your Personal Data

450 |

451 | You have the right to delete or request that We assist in deleting the 452 | Personal Data that We have collected about You. 453 |

454 |

455 | Our Service may give You the ability to delete certain information about 456 | You from within the Service. 457 |

458 |

459 | You may update, amend, or delete Your information at any time by signing 460 | in to Your Account, if you have one, and visiting the account settings 461 | section that allows you to manage Your personal information. You may 462 | also contact Us to request access to, correct, or delete any personal 463 | information that You have provided to Us. 464 |

465 |

466 | Please note, however, that We may need to retain certain information 467 | when we have a legal obligation or lawful basis to do so. 468 |

469 |

Disclosure of Your Personal Data

470 |

Business Transactions

471 |

472 | If the Company is involved in a merger, acquisition or asset sale, Your 473 | Personal Data may be transferred. We will provide notice before Your 474 | Personal Data is transferred and becomes subject to a different Privacy 475 | Policy. 476 |

477 |

Law enforcement

478 |

479 | Under certain circumstances, the Company may be required to disclose 480 | Your Personal Data if required to do so by law or in response to valid 481 | requests by public authorities (e.g. a court or a government agency). 482 |

483 |

Other legal requirements

484 |

485 | The Company may disclose Your Personal Data in the good faith belief 486 | that such action is necessary to: 487 |

488 |
    489 |
  • Comply with a legal obligation
  • 490 |
  • Protect and defend the rights or property of the Company
  • 491 |
  • 492 | Prevent or investigate possible wrongdoing in connection with the 493 | Service 494 |
  • 495 |
  • 496 | Protect the personal safety of Users of the Service or the public 497 |
  • 498 |
  • Protect against legal liability
  • 499 |
500 |

Security of Your Personal Data

501 |

502 | The security of Your Personal Data is important to Us, but remember that 503 | no method of transmission over the Internet, or method of electronic 504 | storage is 100% secure. While We strive to use commercially acceptable 505 | means to protect Your Personal Data, We cannot guarantee its absolute 506 | security. 507 |

508 |

Children's Privacy

509 |

510 | Our Service does not address anyone under the age of 13. We do not 511 | knowingly collect personally identifiable information from anyone under 512 | the age of 13. If You are a parent or guardian and You are aware that 513 | Your child has provided Us with Personal Data, please contact Us. If We 514 | become aware that We have collected Personal Data from anyone under the 515 | age of 13 without verification of parental consent, We take steps to 516 | remove that information from Our servers. 517 |

518 |

519 | If We need to rely on consent as a legal basis for processing Your 520 | information and Your country requires consent from a parent, We may 521 | require Your parent's consent before We collect and use that 522 | information. 523 |

524 |

Links to Other Websites

525 |

526 | Our Service may contain links to other websites that are not operated by 527 | Us. If You click on a third party link, You will be directed to that 528 | third party's site. We strongly advise You to review the Privacy 529 | Policy of every site You visit. 530 |

531 |

532 | We have no control over and assume no responsibility for the content, 533 | privacy policies or practices of any third party sites or services. 534 |

535 |

Changes to this Privacy Policy

536 |

537 | We may update Our Privacy Policy from time to time. We will notify You 538 | of any changes by posting the new Privacy Policy on this page. 539 |

540 |

541 | We will let You know via email and/or a prominent notice on Our Service, 542 | prior to the change becoming effective and update the "Last 543 | updated" date at the top of this Privacy Policy. 544 |

545 |

546 | You are advised to review this Privacy Policy periodically for any 547 | changes. Changes to this Privacy Policy are effective when they are 548 | posted on this page. 549 |

550 |

Contact Us

551 |

552 | If you have any questions about this Privacy Policy, You can contact us: 553 |

554 |
    555 |
  • on Twitter: @e2b_dev
  • 556 |
557 |
558 | ) 559 | } 560 | -------------------------------------------------------------------------------- /frontend/app/twitter-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2b-dev/agentboard/d227c188a083b74d9699f4c9d5ddc53f72dedf95/frontend/app/twitter-image.png -------------------------------------------------------------------------------- /frontend/assets/fonts/Inter-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2b-dev/agentboard/d227c188a083b74d9699f4c9d5ddc53f72dedf95/frontend/assets/fonts/Inter-Bold.woff -------------------------------------------------------------------------------- /frontend/assets/fonts/Inter-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2b-dev/agentboard/d227c188a083b74d9699f4c9d5ddc53f72dedf95/frontend/assets/fonts/Inter-Regular.woff -------------------------------------------------------------------------------- /frontend/components/agent-selector.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Button } from '@/components/ui/button' 3 | import { 4 | DropdownMenu, 5 | DropdownMenuContent, 6 | DropdownMenuItem, 7 | DropdownMenuSeparator, 8 | DropdownMenuTrigger 9 | } from '@/components/ui/dropdown-menu' 10 | import { IconChevronUpDown } from '@/components/ui/icons' 11 | import { ExternalLink } from './external-link' 12 | import { useAgent } from '@/lib/hooks/use-agent' 13 | import { Agents, getInitials, Models } from '@/lib/agents' 14 | 15 | function enumKeys(obj: O): K[] { 16 | return Object.keys(obj).filter(k => !Number.isNaN(k)) as K[] 17 | } 18 | 19 | const AgentModelChoice = ({ 20 | agent, 21 | model 22 | }: { 23 | agent: string 24 | model: string 25 | }) => { 26 | return ( 27 |
28 | 29 | {agent} / {model} 30 | 31 | 32 | {agent} / {model} 33 | 34 |
35 | ) 36 | } 37 | 38 | export const AgentSelector = () => { 39 | const { agent, model, setAgent, setModel } = useAgent() 40 | const choices = [] 41 | for (const agent of enumKeys(Agents)) { 42 | for (const model of enumKeys(Models)) { 43 | choices.push( 44 | { 47 | setAgent(agent) 48 | setModel(model) 49 | }} 50 | > 51 | 52 | 53 | ) 54 | } 55 | } 56 | 57 | return ( 58 |
59 | 60 | 61 | 72 | 73 | e.preventDefault()} 78 | > 79 | {choices} 80 | 81 |
More coming soon...
82 |
83 | 84 | 85 | 86 | Request a new agent 87 | 88 | 89 |
90 |
91 |
92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /frontend/components/anon-shield.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | This component renders above the Chat component if there is no session, allowing the user to view the interface without interacting with it. 3 | If the user clicks anywhere on the anon shield, a modal appears prompting them to sign in. 4 | */ 5 | 'use client' 6 | 7 | import { useState } from 'react' 8 | import { 9 | Dialog, 10 | DialogContent, 11 | DialogDescription, 12 | DialogHeader, 13 | DialogTitle 14 | } from '@/components/ui/dialog' 15 | import { GithubLoginButton, GoogleLoginButton } from '@/components/login-button' 16 | export function AnonShield() { 17 | const [loginDialogOpen, setLoginDialogOpen] = useState(false) 18 | 19 | return ( 20 | <> 21 |
setLoginDialogOpen(true)} 24 | /> 25 | 26 | 27 | 28 | Log in 29 | 30 | Log in to start chatting. 31 |
32 | 33 | 34 |
35 |
36 |
37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /frontend/components/button-scroll-to-bottom.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import { cn } from '@/lib/utils' 6 | import { useAtBottom } from '@/lib/hooks/use-at-bottom' 7 | import { Button, type ButtonProps } from '@/components/ui/button' 8 | import { IconArrowDown } from '@/components/ui/icons' 9 | 10 | export function ButtonScrollToBottom({ className, ...props }: ButtonProps) { 11 | const isAtBottom = useAtBottom() 12 | 13 | return ( 14 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /frontend/components/chat-list.tsx: -------------------------------------------------------------------------------- 1 | import { type Message } from 'ai' 2 | 3 | import { Separator } from '@/components/ui/separator' 4 | import { ChatMessage } from '@/components/chat-message' 5 | import { IconSpinner } from '@/components/ui/icons' 6 | import { AgentsEnum } from '@/lib/agents' 7 | 8 | export interface ChatList { 9 | messages: Message[] 10 | agentType: AgentsEnum 11 | handleSandboxLink: (link: string) => void 12 | isLoading: boolean 13 | } 14 | 15 | export function ChatList({ 16 | messages, 17 | handleSandboxLink, 18 | agentType, 19 | isLoading 20 | }: ChatList) { 21 | if (!messages.length) { 22 | return null 23 | } 24 | 25 | return ( 26 |
27 | {messages.map((message, index) => ( 28 |
29 | 34 | {index < messages.length - 1 && ( 35 | 36 | )} 37 |
38 | ))} 39 | {isLoading && ( 40 |
41 | 42 |
43 | )} 44 |
45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /frontend/components/chat-message-actions.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { type Message } from 'ai' 4 | 5 | import { Button } from '@/components/ui/button' 6 | import { IconCheck, IconCopy } from '@/components/ui/icons' 7 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard' 8 | import { cn } from '@/lib/utils' 9 | 10 | interface ChatMessageActionsProps extends React.ComponentProps<'div'> { 11 | message: Message 12 | } 13 | 14 | export function ChatMessageActions({ 15 | message, 16 | className, 17 | ...props 18 | }: ChatMessageActionsProps) { 19 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }) 20 | 21 | const onCopy = () => { 22 | if (isCopied) return 23 | copyToClipboard(message.content) 24 | } 25 | 26 | return ( 27 |
34 | 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /frontend/components/chat-message.tsx: -------------------------------------------------------------------------------- 1 | // Inspired by Chatbot-UI and modified to fit the needs of this project 2 | // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatMessage.tsx 3 | 4 | import { Message } from 'ai' 5 | import remarkGfm from 'remark-gfm' 6 | import remarkMath from 'remark-math' 7 | 8 | import { cn } from '@/lib/utils' 9 | import { CodeBlock } from '@/components/ui/codeblock' 10 | import { MemoizedReactMarkdown } from '@/components/markdown' 11 | import { 12 | IconOpenAI, 13 | IconUser, 14 | IconOpenInterpreter, 15 | IconDownload 16 | } from '@/components/ui/icons' 17 | import { ChatMessageActions } from '@/components/chat-message-actions' 18 | import { AgentsEnum, ModelsEnum } from '@/lib/agents' 19 | import React from 'react' 20 | 21 | interface ChatMessageProps { 22 | message: Message 23 | agentType: AgentsEnum 24 | handleSandboxLink: (link: string) => void 25 | } 26 | 27 | export function ChatMessage({ 28 | message, 29 | agentType, 30 | handleSandboxLink, 31 | ...props 32 | }: ChatMessageProps) { 33 | return ( 34 |
38 |
46 | {message.role === 'user' ? ( 47 | 48 | ) : agentType === AgentsEnum.OpenInterpreter ? ( 49 | 50 | ) : ( 51 | 52 | )} 53 |
54 |
55 | uri} 59 | components={{ 60 | pre({ node, children, ...props }) { 61 | if (children && Array.isArray(children)) { 62 | const element = children[0] as React.ReactElement< 63 | any, 64 | string | React.JSXElementConstructor 65 | > 66 | const match = /language-(\w+)/.exec( 67 | element?.props.className || '' 68 | ) 69 | 70 | // If the code block is a png, render it as an image 71 | if (match && (match[1] == 'png')) { 72 | return <>{children} 73 | } 74 | } 75 | 76 | return
{children}
77 | }, 78 | a({ children, href }) { 79 | if (href && href.includes('/home/user/')) { 80 | const extractedPath = 81 | '/home/user/' + href.split('/home/user/')[1] 82 | return ( 83 | 90 | ) 91 | } 92 | return {children} 93 | }, 94 | p({ children }) { 95 | return

{children}

96 | }, 97 | code({ node, inline, className, children, ...props }) { 98 | if (children.length) { 99 | if (children[0] == '▍') { 100 | return ( 101 | 102 | ) 103 | } 104 | 105 | children[0] = (children[0] as string).replace('`▍`', '▍') 106 | } 107 | 108 | const match = /language-(\w+)/.exec(className || '') 109 | 110 | if (inline) { 111 | return ( 112 | 113 | {children} 114 | 115 | ) 116 | } 117 | 118 | return ( 119 | 125 | ) 126 | } 127 | }} 128 | > 129 | {message.content} 130 |
131 | 132 |
133 |
134 | ) 135 | } 136 | -------------------------------------------------------------------------------- /frontend/components/chat-panel.tsx: -------------------------------------------------------------------------------- 1 | import { type UseChatHelpers } from 'ai/react' 2 | 3 | import { Button } from '@/components/ui/button' 4 | import { PromptForm } from '@/components/prompt-form' 5 | import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom' 6 | import { IconRefresh, IconStop } from '@/components/ui/icons' 7 | 8 | interface ChatPanelProps 9 | extends Pick< 10 | UseChatHelpers, 11 | 'isLoading' | 'messages' | 'stop' | 'input' | 'setInput' | 'handleSubmit' 12 | > { 13 | fileUploadOnChange: (e: React.ChangeEvent) => void 14 | fileUploading: boolean 15 | id?: string 16 | loggedIn: boolean 17 | sandboxID: string 18 | reload: () => void 19 | } 20 | 21 | export function ChatPanel({ 22 | id, 23 | isLoading, 24 | stop, 25 | reload, 26 | input, 27 | setInput, 28 | messages, 29 | handleSubmit, 30 | fileUploadOnChange, 31 | fileUploading, 32 | loggedIn, 33 | sandboxID 34 | }: ChatPanelProps) { 35 | return ( 36 |
37 | 38 |
39 |
40 | {isLoading ? ( 41 | 49 | ) : ( 50 | messages?.length > 0 && ( 51 | 59 | ) 60 | )} 61 |
62 |
63 | 73 |
74 |
75 |
76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /frontend/components/chat-scroll-anchor.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { useInView } from 'react-intersection-observer' 5 | 6 | import { useAtBottom } from '@/lib/hooks/use-at-bottom' 7 | 8 | interface ChatScrollAnchorProps { 9 | trackVisibility?: boolean 10 | } 11 | 12 | export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) { 13 | const isAtBottom = useAtBottom() 14 | const { ref, entry, inView } = useInView({ 15 | trackVisibility, 16 | delay: 100, 17 | rootMargin: '0px 0px -150px 0px' 18 | }) 19 | 20 | React.useEffect(() => { 21 | if (isAtBottom && trackVisibility && !inView) { 22 | entry?.target.scrollIntoView({ 23 | block: 'start' 24 | }) 25 | } 26 | }, [inView, entry, isAtBottom, trackVisibility]) 27 | 28 | return
29 | } 30 | -------------------------------------------------------------------------------- /frontend/components/chat.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { 4 | useState, 5 | useEffect, 6 | useRef, 7 | ComponentProps, 8 | ChangeEvent, 9 | FormEvent 10 | } from 'react' 11 | import { toast } from 'react-hot-toast' 12 | import { useChat, type Message } from 'ai/react' 13 | import { type User } from '@supabase/supabase-js' 14 | import { createClient } from '@/utils/supabase/client' 15 | import { cn } from '@/lib/utils' 16 | import { ChatList } from '@/components/chat-list' 17 | import { ChatPanel } from '@/components/chat-panel' 18 | import { EmptyScreen } from '@/components/empty-screen' 19 | import { ChatScrollAnchor } from '@/components/chat-scroll-anchor' 20 | import { IconSpinner } from '@/components/ui/icons' 21 | import { CHAT_API_ENDPOINT } from '@/lib/constants' 22 | import { useAgent } from '@/lib/hooks/use-agent' 23 | 24 | interface ChatProps extends ComponentProps<'div'> { 25 | initialMessages?: Message[] 26 | id?: string 27 | user?: User 28 | } 29 | interface SandboxData { 30 | sandboxID: string 31 | } 32 | 33 | const sandboxPort = 49982 34 | 35 | export function Chat({ id, initialMessages, className, user }: ChatProps) { 36 | /* State for sandbox management */ 37 | const [sandboxID, setSandboxID] = useState(null) 38 | 39 | /* State for text input */ 40 | const [pendingMessagesValue, setPendingMessagesValue] = useState< 41 | Message[] | null 42 | >(null) 43 | 44 | /* State for file upload input */ 45 | const [pendingFileInputValue, setPendingFileInputValue] = 46 | useState(null) 47 | const [fileUploading, setFileUploading] = useState(false) 48 | 49 | /* State for dragging files */ 50 | const [dragging, setDragging] = useState(false) 51 | 52 | /* State for chatStreaming */ 53 | const [chatResponseLoading, setChatResponseLoading] = useState(false) 54 | 55 | /* Chat state management */ 56 | const { messages, input, setInput, setMessages } = useChat({ 57 | initialMessages, 58 | id, 59 | body: { id } 60 | }) 61 | const { agent, model } = useAgent() 62 | 63 | const userPressedStopGeneration = useRef(false) 64 | 65 | const supabase = createClient() 66 | 67 | /* Creates sandbox and stores the sandbox ID */ 68 | const fetchSandboxID = async () => { 69 | if (!sandboxID) { 70 | await fetch('/api/create-sandbox', { 71 | method: 'GET', 72 | headers: { 73 | 'Content-Type': 'application/json' 74 | } 75 | }) 76 | .then(res => { 77 | if (!res.ok) 78 | throw new Error( 79 | `HTTP error! status: ${res.status}, response: ${JSON.stringify( 80 | res 81 | )}` 82 | ) 83 | return res.json() 84 | }) 85 | .then((data: unknown) => { 86 | const sandboxData = data as SandboxData 87 | setSandboxID(sandboxData.sandboxID) 88 | }) 89 | .catch(err => { 90 | console.error(err) 91 | }) 92 | } 93 | } 94 | 95 | /* Ensures we only create a sandbox once (even with strictmode doublerendering) */ 96 | const fetchSandboxIDCalled = useRef(false) 97 | useEffect(() => { 98 | if (!fetchSandboxIDCalled.current && !sandboxID && !!user) { 99 | fetchSandboxID().catch(err => console.error(err)) 100 | fetchSandboxIDCalled.current = true 101 | } 102 | }, [sandboxID, user]) 103 | 104 | /* Stores user file input in pendingFileInputValue */ 105 | async function fileUploadOnChange( 106 | e: ChangeEvent | DragEvent 107 | ) { 108 | // indicate to user that file is uploading 109 | 110 | let file: File | undefined | null = null 111 | if (e instanceof DragEvent) { 112 | file = e.dataTransfer?.files[0] 113 | } else if (e && 'target' in e && e.target.files) { 114 | file = e.target.files[0] 115 | } else { 116 | console.log('Error: fileUploadOnChange called with invalid event type') 117 | return 118 | } 119 | let newMessages: Message[] = [ 120 | ...messages, 121 | { 122 | id: id || 'default-id', 123 | content: `Uploading \`${file?.name}\` ...`, 124 | role: 'user' 125 | } 126 | ] 127 | setMessages(newMessages) 128 | setPendingFileInputValue(file ? file : null) 129 | } 130 | 131 | /* Sends pending file input to sandbox after sandbox is created */ 132 | useEffect(() => { 133 | const executePendingFileUploadEvent = () => { 134 | if (sandboxID && pendingFileInputValue) { 135 | if (pendingFileInputValue) { 136 | // needed to disable other user input while file is uploading 137 | setFileUploading(true) 138 | 139 | // prepare file for upload 140 | const formData = new FormData() 141 | formData.append('file', pendingFileInputValue) 142 | formData.append('fileName', pendingFileInputValue.name) 143 | formData.append('sandboxID', sandboxID) 144 | 145 | // upload file 146 | fetch('/api/upload-file', { 147 | method: 'POST', 148 | body: formData 149 | }) 150 | .then(async response => { 151 | if (!response.ok) { 152 | console.log('Error when uploading file - response not ok') 153 | const errorText = await response.text() 154 | console.log(`Error details: ${errorText}`) 155 | throw new Error( 156 | `HTTP error! status: ${response.status}, details: ${errorText}` 157 | ) 158 | } 159 | 160 | // replace file upload message with success message 161 | let newMessages = [ 162 | ...messages, 163 | { 164 | id: id || 'default-id', 165 | content: `Uploaded \`${pendingFileInputValue.name}\` ✅`, 166 | role: 'user' as 'user' 167 | } 168 | ] 169 | setMessages(newMessages) 170 | }) 171 | .catch(error => { 172 | console.error('Error when uploading file:', error) 173 | 174 | // replace file upload message with error message 175 | let newMessages = [ 176 | ...messages, 177 | { 178 | id: id || 'default-id', 179 | content: `Unable to upload \`${pendingFileInputValue.name}\`!`, 180 | role: 'user' as 'user' 181 | } 182 | ] 183 | setMessages(newMessages) 184 | }) 185 | .finally(() => { 186 | setPendingFileInputValue(null) 187 | setFileUploading(false) 188 | 189 | // required to allow user to upload the same file again 190 | // pendingFileInputValue.target.value = '' 191 | }) 192 | } 193 | } 194 | } 195 | executePendingFileUploadEvent() 196 | }, [sandboxID, pendingFileInputValue]) 197 | 198 | /* Attaches supabase listener that clears the message history when user logs out */ 199 | useEffect(() => { 200 | const { data: authListener } = supabase.auth.onAuthStateChange(event => { 201 | if (event === 'SIGNED_OUT') { 202 | // Clear message history when the user logs out or their account is deleted 203 | setMessages([]) 204 | setSandboxID(null) 205 | } 206 | }) 207 | 208 | return () => { 209 | // Cleanup the listener when the component unmounts 210 | authListener.subscription?.unsubscribe() 211 | } 212 | }, []) 213 | 214 | /* Attaches listeners to window to allow user to drag and drop files */ 215 | useEffect(() => { 216 | const dragEvents = ['dragenter', 'dragover', 'dragleave', 'drop'] 217 | const dragHandler = (event: any) => { 218 | event.preventDefault() 219 | event.stopPropagation() 220 | } 221 | const dragStartHandler = (event: DragEvent) => { 222 | setDragging(true) 223 | dragHandler(event) 224 | } 225 | const dragEndHandler = (event: DragEvent) => { 226 | setDragging(false) 227 | dragHandler(event) 228 | } 229 | const dropHandler = (event: DragEvent) => { 230 | setDragging(false) 231 | if (event.dataTransfer && event.dataTransfer.files.length > 0) { 232 | fileUploadOnChange(event) 233 | setFileUploading(true) 234 | event.dataTransfer.clearData() 235 | } 236 | dragHandler(event) 237 | } 238 | dragEvents.forEach(eventName => { 239 | window.addEventListener(eventName, dragHandler) 240 | }) 241 | window.addEventListener('dragover', dragStartHandler) 242 | window.addEventListener('dragleave', dragEndHandler) 243 | window.addEventListener('drop', dropHandler) 244 | return () => { 245 | dragEvents.forEach(eventName => { 246 | window.removeEventListener(eventName, dragHandler) 247 | }) 248 | window.removeEventListener('dragover', dragStartHandler) 249 | window.removeEventListener('dragleave', dragEndHandler) 250 | window.removeEventListener('drop', dropHandler) 251 | } 252 | }, []) 253 | 254 | /* Cancels most recent assistant response and sends most recent user message to sandbox again */ 255 | const reload = () => { 256 | // sometimes no assistant message has come in yet, we will just submit the previous user message 257 | console.log('reload, messages: ', messages) 258 | if (messages[messages.length - 1].role === 'user') { 259 | setChatResponseLoading(true) 260 | submitAndUpdateMessages(messages).catch(err => console.error(err)) 261 | } 262 | // assistant message was the most recent message in the messages array 263 | else { 264 | const updatedMessages = messages.slice(0, -1) 265 | setMessages(updatedMessages) 266 | setChatResponseLoading(true) 267 | submitAndUpdateMessages(updatedMessages).catch(err => console.error(err)) 268 | } 269 | } 270 | 271 | /* utility function to handle the errors when getting a response from the /chat API endpoint */ 272 | const handleChatResponse = (response: Response) => { 273 | if (response.ok) { 274 | return 275 | } 276 | if (response.status === 401) { 277 | toast.error(response.statusText) 278 | } else if (response.status === 500) { 279 | toast.error( 280 | 'Your sandbox closed after 5 minutes of inactivity. Please refresh the page to start a new sandbox.' 281 | ) 282 | } else { 283 | toast.error( 284 | `An unexpected ${response.status} error occurred. Please send your message again after reloading.` 285 | ) 286 | } 287 | } 288 | 289 | const submitAndUpdateMessages = async (updatedMessages: Message[]) => { 290 | // If the session has an expired access token, this method will use the refresh token to get a new session. 291 | const { 292 | data: { session } 293 | } = await supabase.auth.getSession() 294 | 295 | try { 296 | // call the /chat API endpoint 297 | const res = await fetch(CHAT_API_ENDPOINT, { 298 | method: 'POST', 299 | headers: { 300 | 'Content-Type': 'application/json', 301 | Authorization: `Bearer ${session?.access_token}` 302 | }, 303 | body: JSON.stringify({ 304 | agent: agent, 305 | model: model, 306 | messages: updatedMessages 307 | }) 308 | }) 309 | 310 | handleChatResponse(res) 311 | 312 | // check if response was ok 313 | if (!res.ok) { 314 | console.error(`/chat HTTP error! status: ${res.status}`) 315 | return 316 | } 317 | 318 | // parse stream response 319 | const reader = res.body?.getReader() 320 | const previousMessages = JSON.parse(JSON.stringify(updatedMessages)) 321 | let messageBuffer = '' 322 | const textDecoder = new TextDecoder() 323 | 324 | // start the timer when the first byte is received for timeout later 325 | let startTime = Date.now() 326 | 327 | if (reader && !userPressedStopGeneration.current) { 328 | // keep reading from stream until done 329 | while (true) { 330 | const { value, done } = await reader.read() 331 | if (done) { 332 | setChatResponseLoading(false) 333 | break 334 | } 335 | // Load balancer has a timeout of 10 minutes from first to last streamed byte per response. 336 | // This prevents the user from seeing randomly truncated output. 337 | if (Date.now() - startTime > 3 * 60 * 1000) { 338 | // 3 minutes timeout 339 | // Notify the user about the timeout 340 | toast.error( 341 | 'Sorry, the response was too lengthy and it timed out. Try again with a shorter task.' 342 | ) 343 | setChatResponseLoading(false) 344 | await reader.cancel() 345 | break // Exit the loop or handle as needed 346 | } 347 | 348 | // user pressed stop generation 349 | if (userPressedStopGeneration.current) { 350 | await reader.cancel() 351 | break 352 | } 353 | 354 | const text = textDecoder.decode(value) 355 | messageBuffer += decodeURIComponent(text) 356 | 357 | const lastMessage = { 358 | id: id || 'default-id', 359 | content: messageBuffer, 360 | role: 'assistant' as 'assistant' 361 | } 362 | setMessages([...previousMessages, lastMessage]) 363 | } 364 | } 365 | 366 | userPressedStopGeneration.current = false 367 | } catch (error) { 368 | console.log('Error when fetching chat response: ', error) 369 | } 370 | } 371 | 372 | /* Stores user text input in pendingMessageInputValue */ 373 | const handleMessageSubmit = async (e: FormEvent) => { 374 | e.preventDefault() 375 | setChatResponseLoading(true) 376 | if (!input?.trim()) { 377 | return 378 | } 379 | 380 | // add user message to the messages array 381 | const updatedMessages: Message[] = [ 382 | ...messages, 383 | { 384 | id: id || 'default-id', 385 | content: input, 386 | role: 'user' 387 | } as Message 388 | ] 389 | setMessages(updatedMessages) 390 | setInput('') 391 | 392 | if (sandboxID) { 393 | // get the response from the sandbox with the updates messages array 394 | submitAndUpdateMessages(updatedMessages).catch(err => console.error(err)) 395 | } else { 396 | setPendingMessagesValue(updatedMessages) 397 | } 398 | } 399 | 400 | /* Sends the pending message to sandbox once it is created */ 401 | useEffect(() => { 402 | const executePendingSubmitEvent = async () => { 403 | if (sandboxID && pendingMessagesValue) { 404 | submitAndUpdateMessages(pendingMessagesValue).catch(err => 405 | console.error(err) 406 | ) 407 | setPendingMessagesValue(null) 408 | } 409 | } 410 | executePendingSubmitEvent() 411 | }, [sandboxID, pendingMessagesValue]) 412 | 413 | /* Allows the user to download files from the sandbox */ 414 | const handleSandboxLink = (href: string) => { 415 | fetch(`https://${sandboxPort}-${sandboxID}.e2b.dev/file?path=${href}`) 416 | .then(response => { 417 | if (!response.ok) throw new Error('Network response was not ok.') 418 | return response.blob() 419 | }) 420 | .then(blob => { 421 | const url = window.URL.createObjectURL(blob) 422 | const a = document.createElement('a') 423 | a.href = url 424 | a.download = href.split('/').pop() || 'download' 425 | document.body.appendChild(a) 426 | a.click() 427 | a.remove() 428 | }) 429 | .catch(err => { 430 | console.error(err) 431 | }) 432 | } 433 | 434 | const stopEverything = async () => { 435 | setChatResponseLoading(false) 436 | userPressedStopGeneration.current = true 437 | } 438 | return ( 439 | <> 440 | {dragging && ( 441 |
442 |

443 | Drop files to upload 444 |

445 |
446 | )} 447 |
448 | {messages.length ? ( 449 | <> 450 | 456 | {!sandboxID && ( 457 | <> 458 |
459 |

Finishing sandbox bootup...

460 |

461 | 462 |

463 |
464 | 465 | )} 466 | 467 | 468 | ) : ( 469 | 470 | )} 471 |
472 | 473 | 487 | 488 | ) 489 | } 490 | -------------------------------------------------------------------------------- /frontend/components/empty-screen.tsx: -------------------------------------------------------------------------------- 1 | import { UseChatHelpers } from 'ai/react' 2 | 3 | import { Button } from '@/components/ui/button' 4 | import { IconArrowRight } from '@/components/ui/icons' 5 | 6 | const exampleMessages = [ 7 | { 8 | heading: 'Plot a graph of a TESLA stock', 9 | message: `Plot a graph of the TESLA stock performance for the last 30 days` 10 | }, 11 | { 12 | heading: 'File conversion for an image', 13 | message: 'Download photo of the day from NASA website and convert the photo to a png' 14 | }, 15 | { 16 | heading: 'Get audio from a YouTube video', 17 | message: `Extract the audio from this youtube video https://www.youtube.com/watch?v=dQw4w9WgXcQ` 18 | } 19 | ] 20 | 21 | export function EmptyScreen({ setInput }: Pick) { 22 | return ( 23 |
24 |
25 |

Welcome to Agentboard!

26 |

27 | Agentboard is the easiest way to use an AI agent in the browser. 28 |

29 |

30 | Specify a task, and Agentboard will write and execute code to complete 31 | it. 32 |

33 |

34 | You'll find it useful for CSV analysis, image manipulation, or 35 | web scraping. 36 |

37 |

38 | Try one of the following examples: 39 |

40 |
41 | {exampleMessages.map((message, index) => ( 42 | 51 | ))} 52 |
53 |
54 |
55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /frontend/components/external-link.tsx: -------------------------------------------------------------------------------- 1 | export function ExternalLink({ 2 | href, 3 | children 4 | }: { 5 | href: string 6 | children: React.ReactNode 7 | }) { 8 | return ( 9 | 15 | {children} 16 | 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /frontend/components/feedback.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import Script from 'next/script' 4 | 5 | declare global { 6 | namespace JSX { 7 | interface IntrinsicElements { 8 | 'chatlio-widget': any 9 | } 10 | } 11 | } 12 | export function Feedback() { 13 | return ( 14 | <> 15 |