├── .env.example ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── app ├── actions.ts ├── api │ └── chat │ │ └── route.ts ├── favicon.ico ├── layout.tsx └── page.tsx ├── assets └── preview.png ├── components.json ├── components ├── chat │ ├── example-prompts.tsx │ ├── input.tsx │ ├── loader.tsx │ ├── message-list.tsx │ └── message.tsx ├── frame.tsx ├── icons.tsx ├── loader.tsx ├── logo.tsx ├── markdown.tsx ├── providers.tsx ├── repo-banner.tsx ├── scanline.tsx ├── surfing.tsx ├── ui │ ├── badge.tsx │ ├── button.tsx │ ├── card.tsx │ ├── cctv.tsx │ ├── cursor-click.tsx │ ├── grid-pattern.tsx │ ├── input.tsx │ ├── keyboard.tsx │ ├── route.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── shared-menu-styles.ts │ └── skeleton.tsx └── use-scroll-to-bottom.ts ├── lib ├── chat-context.tsx ├── config.ts ├── logger.ts ├── streaming │ ├── anthropic.ts │ ├── index.ts │ ├── openai.ts │ ├── resolution.test.ts │ └── resolution.ts └── utils.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── anthropic.svg ├── google.svg ├── groq.svg ├── mistral.svg ├── openai.svg └── xai.svg ├── readme-assets ├── surf-dark.png └── surf-light.png ├── styles ├── globals.css ├── theme.css └── variables.css ├── tsconfig.json └── types ├── anthropic.ts ├── api.ts └── chat.ts /.env.example: -------------------------------------------------------------------------------- 1 | E2B_API_KEY=your_e2b_api_key 2 | OPENAI_API_KEY=your_openai_api_key -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2025 FoundryLabs, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![E2B Surf Preview Light](/readme-assets/surf-light.png#gh-light-mode-only) 2 | ![E2B Surf Preview Dark](/readme-assets/surf-dark.png#gh-dark-mode-only) 3 | 4 | # 🏄 Surf - OpenAI's Computer Use Agent + E2B Desktop 5 | 6 | A Next.js application that allows AI to interact with a virtual desktop environment. This project integrates [E2B's desktop sandbox](https://github.com/e2b-dev/desktop) with OpenAI's API to create an AI agent that can perform tasks on a virtual computer through natural language instructions. 7 | 8 | [E2B](https://e2b.dev) is an open source isolated virtual computer in the cloud made for AI use cases. 9 | 10 | ## Overview 11 | 12 | The Computer Use App provides a web interface where users can: 13 | 14 | 1. Start a virtual desktop sandbox environment 15 | 2. Send natural language instructions to an AI agent 16 | 3. Watch as the AI agent performs actions on the virtual desktop 17 | 4. Interact with the AI through a chat interface 18 | 19 | The application uses Server-Sent Events (SSE) to stream AI responses and actions in real-time, providing a seamless experience. 20 | 21 | ## How It Works 22 | 23 | ### Architecture 24 | 25 | The application consists of several key components: 26 | 27 | 1. **Frontend UI (Next.js)**: Provides the user interface with a virtual desktop view and chat interface 28 | 2. [**E2B Desktop Sandbox**](https://github.com/e2b-dev/desktop): Creates and manages virtual desktop environments 29 | 3. [**OpenAI Computer Use**](https://platform.openai.com/docs/guides/tools-computer-use): Processes user instructions and generates actions for the AI agent 30 | 4. **Streaming API**: Handles real-time communication between the frontend and backend 31 | 32 | ### Core Flow 33 | 34 | 1. User starts a new sandbox instance 35 | 2. E2B creates a virtual desktop and provides a URL for streaming 36 | 3. User sends instructions via the chat interface 37 | 4. Backend processes the instructions using OpenAI's API 38 | 5. AI generates actions (clicks, typing, etc.) to perform on the virtual desktop 39 | 6. Actions are executed on the sandbox and streamed back to the frontend 40 | 7. The process repeats as the user continues to provide instructions 41 | 42 | ## Prerequisites 43 | 44 | Before starting, you'll need: 45 | 46 | 1. [Node.js](https://nodejs.org/) (version specified in package.json) 47 | 2. [npm](https://www.npmjs.com/) (comes with Node.js) 48 | 3. An [E2B API key](https://e2b.dev/docs/getting-started/api-key) 49 | 4. An [OpenAI API key](https://platform.openai.com/api-keys) 50 | 51 | ## Setup Instructions 52 | 53 | 1. **Clone the repository** 54 | ```bash 55 | git clone https://github.com/e2b-dev/surf 56 | cd surf 57 | ``` 58 | 59 | 2. **Install dependencies** 60 | ```bash 61 | npm install 62 | ``` 63 | 64 | 3. **Set up environment variables** 65 | 66 | Create a `.env.local` file in the root directory based on the provided `.env.example`: 67 | 68 | ```env 69 | E2B_API_KEY=your_e2b_api_key 70 | OPENAI_API_KEY=your_openai_api_key 71 | ``` 72 | 73 | 4. **Start the development server** 74 | ```bash 75 | npm run dev 76 | ``` 77 | 78 | 5. **Open the application** 79 | 80 | Navigate to [http://localhost:3000](http://localhost:3000) in your browser. 81 | 82 | ## Usage 83 | 84 | 1. **Start a Sandbox Instance** 85 | - Click the "Start new Sandbox" button to initialize a virtual desktop environment 86 | - Wait for the sandbox to start (this may take a few seconds) 87 | 88 | 2. **Send Instructions** 89 | - Type your instructions in the chat input (e.g., "Open Firefox and go to google.com") 90 | - Press Enter or click the send button 91 | - You can also select from example prompts if available 92 | 93 | 3. **Watch AI Actions** 94 | - The AI will process your instructions and perform actions on the virtual desktop 95 | - You can see the AI's reasoning and actions in the chat interface 96 | - The virtual desktop will update in real-time as actions are performed 97 | 98 | 4. **Manage the Sandbox** 99 | - The timer shows the remaining time for your sandbox instance 100 | - You can stop the sandbox at any time by clicking the "Stop" button 101 | - The sandbox will automatically extend its time when it's about to expire 102 | 103 | ## Features 104 | 105 | - **Virtual Desktop Environment**: Runs a Linux-based desktop in a sandbox 106 | - **AI-Powered Interaction**: Uses OpenAI's API to understand and execute user instructions 107 | - **Real-Time Streaming**: Shows AI actions and responses as they happen 108 | - **Chat Interface**: Provides a conversational interface for interacting with the AI 109 | - **Example Prompts**: Offers pre-defined instructions to help users get started 110 | - **Dark/Light Mode**: Supports both dark and light themes 111 | 112 | ## Technical Details 113 | 114 | ### Dependencies 115 | 116 | The application uses several key dependencies: 117 | 118 | - **Next.js**: React framework for the frontend 119 | - **@e2b/desktop**: SDK for creating and managing desktop sandbox environments 120 | - **OpenAI**: SDK for interacting with OpenAI's API 121 | - **Tailwind CSS**: Utility-first CSS framework for styling 122 | - **Framer Motion**: Library for animations 123 | 124 | See `package.json` for a complete list of dependencies. 125 | 126 | ### API Endpoints 127 | 128 | - **/api/chat**: Handles chat messages and streams AI responses and actions 129 | 130 | ### Server Actions 131 | 132 | - **createSandbox**: Creates a new sandbox instance 133 | - **increaseTimeout**: Extends the sandbox timeout 134 | - **stopSandboxAction**: Stops a running sandbox instance 135 | 136 | ## Troubleshooting 137 | 138 | - **Sandbox not starting**: Verify your E2B API key is correct in `.env.local` 139 | - **AI not responding**: Check that your OpenAI API key is valid and has access to the required models 140 | - **Actions not working**: Ensure the sandbox is running and the AI has proper instructions 141 | 142 | ## Contributing 143 | 144 | Contributions are welcome! Please feel free to submit a Pull Request. 145 | 146 | ## License 147 | 148 | This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. 149 | 150 | ## Support 151 | 152 | If you encounter any issues or have questions: 153 | - Check the [E2B Documentation](https://e2b.dev/docs) 154 | - Join the [E2B Discord](https://discord.gg/U7KEcGErtQ) 155 | - Open an [issue](https://github.com/e2b-dev/computer-use-app/issues) 156 | -------------------------------------------------------------------------------- /app/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { SANDBOX_TIMEOUT_MS } from "@/lib/config"; 4 | import { Sandbox } from "@e2b/desktop"; 5 | 6 | export async function increaseTimeout(sandboxId: string) { 7 | try { 8 | const desktop = await Sandbox.connect(sandboxId); 9 | await desktop.setTimeout(SANDBOX_TIMEOUT_MS); // 5 minutes 10 | return true; 11 | } catch (error) { 12 | console.error("Failed to increase timeout:", error); 13 | return false; 14 | } 15 | } 16 | 17 | export async function stopSandboxAction(sandboxId: string) { 18 | try { 19 | const desktop = await Sandbox.connect(sandboxId); 20 | await desktop.kill(); 21 | return true; 22 | } catch (error) { 23 | console.error("Failed to stop sandbox:", error); 24 | return false; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { Sandbox } from "@e2b/desktop"; 2 | import { ComputerModel, SSEEvent, SSEEventType } from "@/types/api"; 3 | import { 4 | ComputerInteractionStreamerFacade, 5 | createStreamingResponse, 6 | } from "@/lib/streaming"; 7 | import { SANDBOX_TIMEOUT_MS } from "@/lib/config"; 8 | import { OpenAIComputerStreamer } from "@/lib/streaming/openai"; 9 | import { logError } from "@/lib/logger"; 10 | import { ResolutionScaler } from "@/lib/streaming/resolution"; 11 | 12 | export const maxDuration = 800; 13 | 14 | class StreamerFactory { 15 | static getStreamer( 16 | model: ComputerModel, 17 | desktop: Sandbox, 18 | resolution: [number, number] 19 | ): ComputerInteractionStreamerFacade { 20 | const resolutionScaler = new ResolutionScaler(desktop, resolution); 21 | 22 | switch (model) { 23 | case "anthropic": 24 | // currently not implemented 25 | /* return new AnthropicComputerStreamer(desktop, resolutionScaler); */ 26 | case "openai": 27 | default: 28 | return new OpenAIComputerStreamer(desktop, resolutionScaler); 29 | } 30 | } 31 | } 32 | 33 | export async function POST(request: Request) { 34 | const abortController = new AbortController(); 35 | const { signal } = abortController; 36 | 37 | request.signal.addEventListener("abort", () => { 38 | abortController.abort(); 39 | }); 40 | 41 | const { 42 | messages, 43 | sandboxId, 44 | resolution, 45 | model = "openai", 46 | } = await request.json(); 47 | 48 | const apiKey = process.env.E2B_API_KEY; 49 | 50 | if (!apiKey) { 51 | return new Response("E2B API key not found", { status: 500 }); 52 | } 53 | 54 | let desktop: Sandbox | undefined; 55 | let activeSandboxId = sandboxId; 56 | let vncUrl: string | undefined; 57 | 58 | try { 59 | if (!activeSandboxId) { 60 | const newSandbox = await Sandbox.create({ 61 | resolution, 62 | dpi: 96, 63 | timeoutMs: SANDBOX_TIMEOUT_MS, 64 | }); 65 | 66 | await newSandbox.stream.start(); 67 | 68 | activeSandboxId = newSandbox.sandboxId; 69 | vncUrl = newSandbox.stream.getUrl(); 70 | desktop = newSandbox; 71 | } else { 72 | desktop = await Sandbox.connect(activeSandboxId); 73 | } 74 | 75 | if (!desktop) { 76 | return new Response("Failed to connect to sandbox", { status: 500 }); 77 | } 78 | 79 | desktop.setTimeout(SANDBOX_TIMEOUT_MS); 80 | 81 | try { 82 | const streamer = StreamerFactory.getStreamer( 83 | model as ComputerModel, 84 | desktop, 85 | resolution 86 | ); 87 | 88 | if (!sandboxId && activeSandboxId && vncUrl) { 89 | async function* stream(): AsyncGenerator> { 90 | yield { 91 | type: SSEEventType.SANDBOX_CREATED, 92 | sandboxId: activeSandboxId, 93 | vncUrl: vncUrl as string, 94 | }; 95 | 96 | yield* streamer.stream({ messages, signal }); 97 | } 98 | 99 | return createStreamingResponse(stream()); 100 | } else { 101 | return createStreamingResponse(streamer.stream({ messages, signal })); 102 | } 103 | } catch (error) { 104 | logError("Error from streaming service:", error); 105 | 106 | return new Response( 107 | "An error occurred with the AI service. Please try again.", 108 | { status: 500 } 109 | ); 110 | } 111 | } catch (error) { 112 | logError("Error connecting to sandbox:", error); 113 | return new Response("Failed to connect to sandbox", { status: 500 }); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2b-dev/surf/f8ae4ae1ee898538b4f737f58c8ab2ecc0739f5a/app/favicon.ico -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css"; 2 | 3 | import { Metadata } from "next"; 4 | import { Toaster } from "sonner"; 5 | import { Providers } from "../components/providers"; 6 | import { IBM_Plex_Sans, IBM_Plex_Mono } from "next/font/google"; 7 | import { ChatProvider } from "@/lib/chat-context"; 8 | import { Analytics } from "@vercel/analytics/react"; 9 | 10 | const ibmPlexSans = IBM_Plex_Sans({ 11 | subsets: ["latin"], 12 | weight: ["400", "500", "600", "700"], 13 | variable: "--font-ibm-plex-sans", 14 | }); 15 | 16 | const ibmPlexMono = IBM_Plex_Mono({ 17 | subsets: ["latin"], 18 | weight: ["400", "500", "600", "700"], 19 | variable: "--font-ibm-plex-mono", 20 | }); 21 | 22 | export const metadata: Metadata = { 23 | title: "Surf - E2B Computer Use Agent", 24 | description: 25 | "AI agent that interacts with a virtual desktop environment through natural language instructions", 26 | keywords: [ 27 | "AI", 28 | "desktop", 29 | "automation", 30 | "E2B", 31 | "OpenAI", 32 | "virtual desktop", 33 | "sandbox", 34 | ], 35 | authors: [{ name: "E2B", url: "https://e2b.dev" }], 36 | }; 37 | 38 | export default function RootLayout({ 39 | children, 40 | }: { 41 | children: React.ReactNode; 42 | }) { 43 | return ( 44 | 45 | 49 | 50 | 51 | 52 | {children} 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRef, useState, useEffect } from "react"; 4 | import { 5 | MoonIcon, 6 | SunIcon, 7 | Timer, 8 | Power, 9 | Menu, 10 | X, 11 | ArrowUpRight, 12 | } from "lucide-react"; 13 | import { useTheme } from "next-themes"; 14 | import { toast } from "sonner"; 15 | import { increaseTimeout, stopSandboxAction } from "@/app/actions"; 16 | import { motion, AnimatePresence } from "framer-motion"; 17 | import { ChatList } from "@/components/chat/message-list"; 18 | import { ChatInput } from "@/components/chat/input"; 19 | import { ExamplePrompts } from "@/components/chat/example-prompts"; 20 | import { useChat } from "@/lib/chat-context"; 21 | import Frame from "@/components/frame"; 22 | import { Button } from "@/components/ui/button"; 23 | import { Loader, AssemblyLoader } from "@/components/loader"; 24 | import Link from "next/link"; 25 | import Logo from "@/components/logo"; 26 | import { RepoBanner } from "@/components/repo-banner"; 27 | import { SANDBOX_TIMEOUT_MS } from "@/lib/config"; 28 | import { Surfing } from "@/components/surfing"; 29 | 30 | export default function Home() { 31 | const [sandboxId, setSandboxId] = useState(null); 32 | const [isLoading, setIsLoading] = useState(false); 33 | const [vncUrl, setVncUrl] = useState(null); 34 | const { theme, setTheme } = useTheme(); 35 | const [timeRemaining, setTimeRemaining] = useState( 36 | SANDBOX_TIMEOUT_MS / 1000 37 | ); 38 | const [isTabVisible, setIsTabVisible] = useState(true); 39 | const iframeRef = useRef(null); 40 | const iFrameWrapperRef = useRef(null); 41 | const [mobileMenuOpen, setMobileMenuOpen] = useState(false); 42 | 43 | const { 44 | messages, 45 | isLoading: chatLoading, 46 | input, 47 | setInput, 48 | sendMessage, 49 | stopGeneration, 50 | clearMessages, 51 | handleSubmit, 52 | onSandboxCreated, 53 | } = useChat(); 54 | 55 | useEffect(() => { 56 | const handleVisibilityChange = () => { 57 | setIsTabVisible(document.visibilityState === "visible"); 58 | }; 59 | 60 | setIsTabVisible(document.visibilityState === "visible"); 61 | 62 | document.addEventListener("visibilitychange", handleVisibilityChange); 63 | 64 | return () => { 65 | document.removeEventListener("visibilitychange", handleVisibilityChange); 66 | }; 67 | }, []); 68 | 69 | const stopSandbox = async () => { 70 | if (sandboxId) { 71 | try { 72 | stopGeneration(); 73 | const success = await stopSandboxAction(sandboxId); 74 | if (success) { 75 | setSandboxId(null); 76 | setVncUrl(null); 77 | clearMessages(); 78 | setTimeRemaining(SANDBOX_TIMEOUT_MS / 1000); 79 | toast("Sandbox instance stopped"); 80 | } else { 81 | toast.error("Failed to stop sandbox instance"); 82 | } 83 | } catch (error) { 84 | console.error("Failed to stop sandbox:", error); 85 | toast.error("Failed to stop sandbox"); 86 | } 87 | } 88 | }; 89 | 90 | const handleIncreaseTimeout = async () => { 91 | if (!sandboxId) return; 92 | 93 | try { 94 | await increaseTimeout(sandboxId); 95 | setTimeRemaining(SANDBOX_TIMEOUT_MS / 1000); 96 | toast.success("Instance time increased"); 97 | } catch (error) { 98 | console.error("Failed to increase time:", error); 99 | toast.error("Failed to increase time"); 100 | } 101 | }; 102 | 103 | const onSubmit = (e: React.FormEvent) => { 104 | const content = handleSubmit(e); 105 | if (content) { 106 | const width = 107 | iFrameWrapperRef.current?.clientWidth || 108 | (window.innerWidth < 768 ? window.innerWidth - 32 : 1024); 109 | const height = 110 | iFrameWrapperRef.current?.clientHeight || 111 | (window.innerWidth < 768 112 | ? Math.min(window.innerHeight * 0.4, 400) 113 | : 768); 114 | 115 | sendMessage({ 116 | content, 117 | sandboxId: sandboxId || undefined, 118 | environment: "linux", 119 | resolution: [width, height], 120 | }); 121 | } 122 | }; 123 | 124 | const handleExampleClick = (prompt: string) => { 125 | const width = 126 | iFrameWrapperRef.current?.clientWidth || 127 | (window.innerWidth < 768 ? window.innerWidth - 32 : 1024); 128 | const height = 129 | iFrameWrapperRef.current?.clientHeight || 130 | (window.innerWidth < 768 ? Math.min(window.innerHeight * 0.4, 400) : 768); 131 | 132 | sendMessage({ 133 | content: prompt, 134 | sandboxId: sandboxId || undefined, 135 | environment: "linux", 136 | resolution: [width, height], 137 | }); 138 | }; 139 | 140 | const handleSandboxCreated = (newSandboxId: string, newVncUrl: string) => { 141 | setSandboxId(newSandboxId); 142 | setVncUrl(newVncUrl); 143 | setTimeRemaining(SANDBOX_TIMEOUT_MS / 1000); 144 | toast.success("Sandbox instance created"); 145 | }; 146 | 147 | const handleClearChat = () => { 148 | clearMessages(); 149 | toast.success("Chat cleared"); 150 | }; 151 | 152 | const ThemeToggle = () => ( 153 | 165 | ); 166 | 167 | useEffect(() => { 168 | if (!sandboxId) return; 169 | const interval = setInterval(() => { 170 | if (isTabVisible) { 171 | setTimeRemaining((prev) => (prev > 0 ? prev - 1 : 0)); 172 | } 173 | }, 1000); 174 | return () => clearInterval(interval); 175 | }, [sandboxId, isTabVisible]); 176 | 177 | useEffect(() => { 178 | if (!sandboxId) return; 179 | 180 | if (timeRemaining === 10 && isTabVisible) { 181 | handleIncreaseTimeout(); 182 | } 183 | 184 | if (timeRemaining === 0) { 185 | setSandboxId(null); 186 | setVncUrl(null); 187 | clearMessages(); 188 | stopGeneration(); 189 | toast.error("Instance time expired"); 190 | setTimeRemaining(SANDBOX_TIMEOUT_MS / 1000); 191 | } 192 | }, [timeRemaining, sandboxId, stopGeneration, clearMessages, isTabVisible]); 193 | 194 | useEffect(() => { 195 | onSandboxCreated((newSandboxId: string, newVncUrl: string) => { 196 | handleSandboxCreated(newSandboxId, newVncUrl); 197 | }); 198 | }, [onSandboxCreated]); 199 | 200 | return ( 201 |
202 | 208 |
209 |
210 | 215 | 216 |

Surf - Computer Agent by

217 | 218 | 223 | E2B 224 | 225 |
226 | 227 |
228 | 240 |
241 | 242 |
243 | 244 | 245 | 246 | 247 | {sandboxId && ( 248 | 255 | 279 | 280 | 288 | 289 | )} 290 | 291 |
292 | 293 |
294 | 295 | {sandboxId && ( 296 | 303 | 328 | 329 | 337 | 338 | )} 339 | 340 |
341 |
342 | 343 | 344 | {mobileMenuOpen && ( 345 | 352 |
353 | 354 | 355 |
356 |
357 | )} 358 |
359 | 360 |
361 |
365 | {isLoading || (chatLoading && !sandboxId) ? ( 366 |
367 |
368 |

369 | {isLoading ? "Starting instance" : "Creating sandbox..."} 370 |

371 | 372 |
373 | 374 | 381 | 382 |

383 | {isLoading 384 | ? "Preparing your sandbox environment..." 385 | : "Creating a new sandbox for your request..."} 386 |

387 |
388 | ) : sandboxId && vncUrl ? ( 389 |